In a form, I have a Tags field, which is a just a standard text field. A user can type a Tag name and it is added to the article.
I already have three tables: tags
, taggables
and articles
and they are linked via Eloquent relationship methods, given the setup in a previous question I asked.
This is my update method in my ArticleController
/**
* Update the specified resource in storage.
*
* @param \Illuminate\Http\Request $request
* @param int $id
* @return \Illuminate\Http\Response
*/
public function update(Request $request, $id)
{
$validatedData = $request->validate([
'title' => 'required',
'excerpt' => 'required',
]);
$article = Article::find($id);
$article->title = $request->get('title');
$article->author = $request->get('author');
$article->category = $request->get('category');
$article->excerpt = $request->get('excerpt');
$article->content = $request->get('content');
$article->featuredImage = $request->get('featuredImage');
$article->featuredVideo = $request->get('featuredVideo');
$article->readingTime = $request->get('readingTime');
$article->published = $request->get('published');
$article->save();
/**
* Once the article has been saved, we deal with the tag logic.
* Grab the tag or tags from the field, sync them with the article
*/
$tags = $request->get('tags');
$comma = ',';
if (!empty($tags)) {
if (strpos($tags, $comma) !== false) {
$tagList = explode(",", $tags);
// Loop through the tag array that we just created
foreach ($tagList as $tags) {
// Get any existing tags
$tag = Tag::where('name', '=', $tags)->first();
// If the tag exists, sync it, otherwise create it
if ($tag != null) {
$article->tags()->sync($tag->id);
} else {
$tag = new Tag();
$tag->name = $tags;
$tag->slug = str_slug($tags);
$tag->save();
$article->tags()->sync($tag->id);
}
}
} else {
// Only one tag
$tag = Tag::where('name', '=', $tags)->first();
if ($tag != null) {
$article->tags()->sync($tag->id);
} else {
$tag = new Tag();
$tag->name = $tags;
$tag->slug = str_slug($tags);
$tag->save();
$article->tags()->sync($tag->id);
}
}
}
return back();
return redirect()->back();
}
In the section of this method that looks for tags, I do the following things:
explode()
to convert the string into an arrayThis approach feels very messy, however, is there any way I could make this cleaner?
An update, given the answers provided
I went with the following approach:
/**
* Store a newly created resource in storage.
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\Response
*/
public function store(Request $request)
{
$validatedData = $request->validate([
'title' => 'required',
'excerpt' => 'required',
]);
$article = new Article();
$article->title = $request->get('title');
$article->author = $request->get('author');
$article->category = $request->get('category');
$article->excerpt = $request->get('excerpt');
$article->content = $request->get('content');
$article->featuredImage = $request->get('featuredImage');
$article->featuredVideo = $request->get('featuredVideo');
$article->readingTime = $request->get('readingTime');
$article->published = $request->get('published');
//If no featured image set, automatically create featured image placeholder
if ($request->get('featuredImage') == null) {
$article->featuredImage = "http://via.placeholder.com/350x150";
}
$article->save();
// Handle Tags
$tags = $request->get('tags');
if (!empty($tags)) {
$tagList = array_filter(explode(",", $tags));
// Loop through the tag array that we just created
foreach ($tagList as $tags) {
$tag = Tag::firstOrCreate(['name' => $tags, 'slug' => str_slug($tags)]);
}
$tags = Tag::whereIn('name', $tagList)->get()->pluck('id');
$article->tags()->sync($tags);
}
return redirect('editable/news-and-updates')->with('success', 'Article has been added');
}
And then, to display the tags when updating, I did the following:
/**
* Show the form to edit this resource
*/
public function edit($id)
{
$user = auth::user();
$article = Article::find($id);
// Get the tags associated with this article and convert to a comma seperated string
if ($article->has('tags')) {
$tags = $article->tags->pluck('name')->toArray();
$tags = implode(', ', $tags);
} else {
$tags = "";
}
return view('editable.news.edit', compact('article', 'user', 'tags'));
}
Essentially, I just grab the tags associated with the article, convert them to an array, then use implode()
.
This gives me the tags as a comma separated list in the tags field like:
blue, red, orange
However, when updating, If I try to save with the same tags in the field I get:
SQLSTATE[23000]: Integrity constraint violation: 1062 Duplicate entry 'sauce' for key 'tags_slug_unique' (SQL: insert into
tags(
name,
slug,
updated_at,
created_at) values ( sauce, sauce, 2018-05-26 11:42:17, 2018-05-26 11:42:17))
Here is the tag migration for reference:
<?php
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class CreateTagsTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('tags', function (Blueprint $table) {
$table->increments('id');
$table->string('name');
$table->string('slug')->unique();
$table->timestamps();
});
Schema::create('taggables', function (Blueprint $table) {
$table->increments('id');
$table->integer('tag_id')->unsigned();
$table->integer('taggable_id')->unsigned();
$table->string('taggable_type');
$table->foreign('tag_id')->references('id')->on('tags')->onDelete('cascade');
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('taggables');
Schema::dropIfExists('tags');
}
}
Maybe something like this, typed just from head, not tested, but i hope it kinda give you an idea
public function update(Request $request, Article $article)
{
$validatedData = $request->validate([
'title' => 'required',
'excerpt' => 'required',
// Make validation for all inputs !
]);
// Fill model with inputs and save (make sure that inputs are in fillable model property)
$article->fill($request->all())->save();
// Handle Tags
$this->handleTags($request, $article);
// Return Redirect to previous URL
return redirect()->back();
}
/**
* Handle Tags for Article
* @param \Illuminate\Http\Request $request
* @param \App\Article $article
* @return void
*/
public function handleTags(Request $request, Article $article){
/**
* Once the article has been saved, we deal with the tag logic.
* Grab the tag or tags from the field, sync them with the article
*/
$tagsNames = explode(',', $request->get('tags'));
// Create all tags (unassociet)
foreach($tagsNames as $tagName){
Tag::firstOrCreate(['name' => $tagName, 'slug' => str_slug($tagName)])->save();
}
// Once all tags are created we can query them
$tags = Tag::whereIn('name', $tagsNames)->get()->pluck('id')->get();
$article->tags()->sync($tags);
}
There is really no need to check if there are commas and have two different paths. If there are no commas, explode will return one element to iterate over. You can literally just remove the if and the else.
$tagList = explode(",", $tags);
// Loop through the tag array that we just created
foreach ($tagList as $tags) {
// Get any existing tags
$tag = Tag::where('name', '=', $tags)->first();
// If the tag exists, sync it, otherwise create it
if ($tag != null) {
$article->tags()->sync($tag->id);
} else {
$tag = new Tag();
$tag->name = $tags;
$tag->slug = str_slug($tags);
$tag->save();
$article->tags()->sync($tag->id);
}
}
Additionally, there is the ability to do firstOrCreate
which you can see the documentation for here.
The firstOrCreate method will attempt to locate a database record using the given column / value pairs. If the model can not be found in the database, a record will be inserted with the attributes from the first parameter, along with those in the optional second parameter.
This can be use to refactor code to the following:
$tagList = explode(",", $tags);
// Loop through the tag array that we just created
foreach ($tagList as $tags) {
$tag = Tag::firstOrCreate(['slug' => $tags];
}
$tags = Tag::whereIn('name', $tagList)->get()->pluck('id')->get();
$article->tags()->sync($tags);
I think the Easiest way to do tagging is by using many-to-many polymorphic relationship... morphedByMany()
and morphToMany()
.
See this example code...
In migration their are 3 tables
articles
,tags
,taggables
# --- for Article Table ---
public function up()
{
Schema::create('articles', function (Blueprint $table) {
$table->increments('id');
$table->string('title');
// ---
});
}
# --- for Tags Table ---
public function up()
{
Schema::create('tags', function (Blueprint $table) {
$table->increments('id');
$table->string('tagname');
});
}
# --- for Taggables Table ---
public function up()
{
Schema::create('taggables', function (Blueprint $table) {
$table->integer('tag_id');
$table->integer('taggable_id'); // for storing Article ID's
$table->string('taggable_type'); // Aside from Article, if you decide to use tags on other model eg. Videos, ...
});
}
In the Model
# Tag.php Model
class Tag extends Model
{
protected $fillable = [
'tagname',
];
public function article()
{
return $this->morphedByMany('Yourapp\Article', 'taggable');
}
}
# Article.php Model
class Article extends Model
{
protected $fillable = [
'title',
# and more...
];
public function tags()
{
return $this->morphToMany('Yourapp\Tag', 'taggable');
}
}
In the AppServiceProvide.php ~ Yourapp/app/Providers/AppServiceProvider.php
public function boot()
{
//... by creating this map you don't need to store the "Yourapp\Post" to the "taggable_type" on taggable table
Relation::morphMap([
'article' => 'Yourapp\Article',
'videos' => 'Yourapp\Videos', // <--- other models may have tags
]);
}
Now using Elequent you can easily access data
$article->tags; # retrieve related tags for the article
$tags->articles; # or $tags->articles->get() retrieve all article that has specific tag
For storing and updating article
# SAVING article with tags
public function store(Request $request)
{
$validatedData = $request->validate([
'title' => 'required',
//----
// Validate tags and also it should be an Array but its up to you
'tag' => 'required|array|exists:tags,id' # < (exist:tags,id) This will check if tag exist on the Tag table
]);
$article = Article::create([
'title' => $request->input('title'),
//----
]);
//Adding tags to article, Sync() the easy way
$article->tags()->sync($request->input('tag'));
return "Return anywhare";
}
# UPDATE tags article
public function update(Request $request, $id)
{
// Validate first and ...
$article = Article::find($id)->first();
$article->title = $request->input('title');
$article->save();
//Updating tags of the article, Sync() the easy way
$article->tags()->sync($request->input('tag'));
return "Return anywhare";
}
For more details about many-to-many polymorphic relationship
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With