Click to share! ⬇️

laravel belongs to many

Let’s talk about the belongsToMany relationship. Recently we created a really cool introduction to creating a link sharing website in Laravel. The tutorial covers many concepts including routing, models, controllers, and view creation. We even covered some basic relationships in Eloquent. One thing we didn’t cover was applying many to many relations using the belongsToMany method. Let’s see how we might go about doing this now.

  1. #Add The belongsToMany To Your first Model.
  2. #Create The Associated Eloquent Model.
  3. #Create A Migration For The Tags Table and the Pivot Table.
  4. #Migrate The Database.
  5. #Configure The Inverse belongsToMany On The Tag Model.
  6. #Test Our Relationship With Tinker.
  7. #Update The create() method Method For Tag Support with lists().
  8. #Update The Create Form View For Tag Support.
  9. #Update The store() method for Tag Support with attach().
  10. #Update The edit() method for Tag Support with lists().
  11. #Update The Edit Form View for Tag Support.
  12. #Update The update() method for Tag Support with sync().
  13. #Update The Show View For Tag Support.
  14. #Make The Tag Select Box Pretty.

# Step 1: Add The belongsToMany To Your first Model

In this belongsToMany example, we are dealing with a Link.php eloquent model and we want to be able to associate that model with many tags as needed. We simply open up our Link.php model, and add the following snippet.

    /**
     * Each link can have many tags.
     *
     */
    public function tags()
    {
        return $this->belongsToMany('AppTag')->withTimeStamps();
    }

# Step 2: Create The Associated Eloquent Model

We just added a belongsToMany relation in the Link model which references the Tag model. That Tag model does not yet exist, so let’s create it now.

vagrant@homestead:~/Code/laravel$ php artisan make:model Tag


# Step 3: Create A Migration For The Tags Table and the Pivot Table.

We know that many to many relations require a pivot table. That is to say, we need an arbitrary table to perform lookups on to see what Link is related to what Tag and vice versa.

vagrant@homestead:~/Code/laravel$ php artisan make:migration create_tags_table --create=tags

Disclaimer: New migrations are easy, however adding migrations to existing projects scares the daylights out of me. Running migrations on a populated database can be a little dicey when you’re not sure what the result will be. The takeaway? Back up your database early and often!

Anywhoo, the migration file is created for us, but not yet populated with the right Schema. We need to do this on our own, so let’s do that now. We will:

  • add a string type for the name of the tag
  • add Schema to allow for the pivot table
  • follow the naming convention for pivot tables
  • set up two columns, one for link_id, and one for tag_id
  • add timestamps to the pivot table
  • add the ability to cascade deletes if a link gets deleted via foreign key

Note: The convention for a pivot table is to use the singular version of the two tables we are trying to connect, and they are in alphabetical order. In our case we have a links table and a tags table we would like to join. So if we apply the singular versions in alphabetical order joined with an underscore, that gives us link_tag. Here is the result of our goals.

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->timestamps();
        });

        Schema::create('link_tag', function (Blueprint $table) {
            $table->integer('link_id')->unsigned()->index();
            $table->foreign('link_id')->references('id')->on('links')->onDelete('cascade');

            $table->integer('tag_id')->unsigned()->index();
            $table->foreign('tag_id')->references('id')->on('tags')->onDelete('cascade');

            $table->timestamps();
        });
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::drop('tags');
        Schema::drop('link_tag');
    }
}

# Step 4: Migrate The Database

This is a pretty easy step, just type php artisan migrate at the terminal.


# Step 5: Configure The Inverse belongsToMany On The Tag Model

We already configured our Links.php model to allow for a belongsToMany relationship. This means a link can have many tags. The same is true for a tag. A tag can have many links. Let’s now configure our Tag.php model to allow for this.

class Tag extends Model
{
    /**
     * Each tag can have many links.
     *
     */
    public function links()
    {
        return $this->belongsToMany('AppLink');
    }
}

# Step 6: Test Our Relationship With Tinker

With Laravel, it is really easy to test things at the command line using php artisan tinker. Let’s use tinker to test our new functionality now. Pay attention to the bold methods, as they are important when dealing with the belongsToMany relationship.

vagrant@homestead:~/Code/laravel$ php artisan tinker
Create a new tag.

>>> $tag = new AppTag;

Assign it a name.
>>> $tag->name = 'jQuery';
=> "jQuery"

Save that tag.

>>> $tag->save();
=> true
>>> AppTag::all()->toArray();
=> [
       [
           "id"         => 1,
           "name"       => "jQuery",
           "created_at" => "2015-06-23 14:28:55",
           "updated_at" => "2015-06-23 14:28:55"
       ]
   ]
>>>

List the tags.

>>> AppTag::lists('name');
=>  [
       "jQuery"
   ]
>>>

Create another tag, name, and save it.

>>> $tag = new AppTag;
=>  {}
>>> $tag->name = 'php';
=> "php"
>>> $tag->save();
=> true
>>>

List our tags again. Notice we now have two.

>>> AppTag::lists('name');
=>  [
       "jQuery",
       "php"
   ]
>>>

Fetch the first link from our database. (Note: we already had this link in our database – create your own to follow along.)

>>> $link = AppLink::first()
=>  {
       id: 6,
       category_id: 1,
       name: "Laravel PHP Framework",
       url: "http://laravel.com",
       created_at: "2015-06-06 21:41:33",
       updated_at: "2015-06-12 00:52:05",
       user_id: 1,
       slug: "laravel-php-framework",
       outbound_count: 3,
       description: "Laravel is a modern and powerful PHP framework with elegant syntax for rapid application development and prototyping.  Laravel makes use of the best packages available in the PHP community to provide a fantastic development environment for users of all skill levels."
   }
>>>

Associate a tag with that link.

>>> $link->tags()->attach('2');
=> null
>>>

Fetch all the entries in our pivot table (it worked).

>>> DB::select('select * from link_tag');
=> [
        {
           link_id: 6,
           tag_id: 2,
           created_at: "2015-06-23 14:48:29",
           updated_at: "2015-06-23 14:48:29"
       }
   ]
>>>

Display the tags associated with this link.

>>> $link->tags->toArray();
=> [
       [
           "id"         => 2,
           "name"       => "php",
           "created_at" => "2015-06-23 14:41:10",
           "updated_at" => "2015-06-23 14:41:10",
           "pivot"      => [
               "link_id"    => 6,
               "tag_id"     => 2,
               "created_at" => "2015-06-23 14:48:29",
               "updated_at" => "2015-06-23 14:48:29"
           ]
       ]
   ]
>>>

List the names of any tags on this link.

>>> $link->tags->lists('name');
=>  [
       "php"
   ]
>>>

Find the php tag in the database.

>>> $tag = AppTag::find('2');
=>  {
       id: 2,
       name: "php",
       created_at: "2015-06-23 14:41:10",
       updated_at: "2015-06-23 14:41:10"
   }
>>>

Display which links are associated with this tag.

>>> $tag->links->toArray();
=> [
       [
           "id"             => 6,
           "category_id"    => 1,
           "name"           => "Laravel PHP Framework",
           "url"            => "http://laravel.com",
           "created_at"     => "2015-06-06 21:41:33",
           "updated_at"     => "2015-06-12 00:52:05",
           "user_id"        => 1,
           "slug"           => "laravel-php-framework",
           "outbound_count" => 3,
           "description"    => "Laravel is a modern and powerful PHP framework with elegant syntax for rapid application development and prototyping.
 Laravel makes use of the best packages available in the PHP community to provide a fantastic development environment for users of all skill levels.",

           "pivot"          => [
               "tag_id"  => 2,
               "link_id" => 6
           ]
       ]
   ]
>>>

Pretty sweet. By using tinker, we can see that all of the new functionality we added for the belongsToMany relationship on the Link.php and Tag.php models are working.


# Step 7: Update The Create Controller Method For Tag Support

In the prior step, we do have a small proof of concept going as it appears to work correctly when using Tinker. Our view which displays the form to create a new link is going to be expecting a $tags variable which we can use as the second argument to Form::select. Let’s make sure we provide them with the code in this method.

    public function create()
    {
        $tags = AppTag::lists('name', 'id');
        $categories = DB::table('categories')->lists('name', 'id');
        return view('link.create', compact(['tags', 'categories']));
    }

This step makes sure that we select all of the available tags to select from when we visit the view to create a new link.


# Step 8: Update The Create Form For Tag Support

Now we can allow for the belongsToMany tag support in the UI. In the form that we use to create a new link, let’s add the following snippet.

<div class="form-group">
     {!! Form::label('tags', 'Select associated tags (hold ctrl for multiple):') !!}
     {!! Form::select('tags[]', $tags, null, ['class' => 'form-control input-lg', 'multiple', 'id' => 'prettify', 'data-placeholder' => 'Choose at least one tag']) !!}
</div>

Pay attention to the Form::select argument list. The first argument is the name of the select element. We add the array notation to it to allow for this select element to handle passing multiple values at once via an array. The second argument are the default values that will populate the list. We set this to $tags, and we showed how to populate that variable in the prior step. The third argument specifies the selected item(s) in the list. For a new entry we can leave this at null. For an update style form, this will need to have the values of the currently selected tags. The fourth argument is the familiar method of passing an array which contains any additional attributes and values that need to be assigned to the element.

With both our form and controller now updated to support tags via our belongsToMany work, we can check in the browser to make sure everything is working.

Laravel belongsToMany Example


# Step 9: Update The store() method for Tag Support

Now that we have our database ready to support tags with many to many relations and a pivot table, along with a form that displays the tags for us to select, we need to make sure that when we submit the form to store a new entry in the database that it handles processing the tags correctly. It may look something like this:

public function store(LinkCreateFormRequest $request)
{

	$link = new Link(array(
		'name' => $request->get('name'),
		'url' => $request->get('url'),
		'description' => $request->get('description')
	));

	$link->category()->associate(Category::find($request->get('category')));

	$link->user()->associate(User::find(Auth::id()));

	$link->save();

	$link->tags()->attach($request->input('tags'));

	return Redirect::route('link.show',
		array($link->slug))->with('message', 'Your link has been added!');

}

The key method you need to pay attention to is the attach() method. This is what handles accepting the tags input, and associating it to the given resource. The attach() method is perfect for creating the initial resource into the database.


# Step 10: Update The edit() method for Tag Support.

This could be handled with Route Model Binding, but we do not yet have this set up in our project. Here is an example of how you might populate the currently selected tags for use in an edit or update style form. Or course we could refactor this to make it much better, but for now, this will do.

public function edit($id)
{
	$link = Link::findBySlug($id);
	$tags = AppTag::lists('name', 'id');

	foreach ($link->tags as $tag) {
		$currentTags[] = $tag->id;
	}

	if(empty($currentTags)){
		$currentTags = '';
	}

	$categories = DB::table('categories')->lists('name', 'id');

	return view('link.edit', compact(['link','title','metadescription','categories','tags','currentTags']));
}

# Step 11: Update The Edit Form View for Tag Support.

In this step, we make sure that the third argument includes all of the values for the currently selected tags, $currentTags. This way, if you have a resource which has several tags already selected, when you go to edit that resource in a form, this will be reflected in the user interface and you will be able to add and remove tags as needed from there.

<div class="form-group">
  {!! Form::label('tags', 'Select associated tags (hold ctrl for multiple):') !!}
  {!! Form::select('tags[]', $tags, $currentTags, ['class' => 'form-control input-lg', 'multiple', 'id' => 'prettify']) !!}
</div>

# Step 12: Update The update() method for Tag Support

When dealing with a many to many relationship in the context of a resource update, attach might not be the best solution. In this case we want to be able to simultaneously delete and add items to the pivot table during an update. In other words, we want to sync the database with our input. In this case we use the sync() method. An example of this might be something like this:

public function update(LinkUpdateFormRequest $request, $id)
{
	$link = Link::findBySlug($id);

	$link->category()->associate(Category::find($request->get('category')));

	$link->save();

	$tags = $request->input('tags');

	$link->tags()->sync($tags);

	return Redirect::route('link.show',
		array($link->slug))->with('message', 'Your link has been updated!');
}

# Step 13: Update The Show View For Tag Support

In a moment, we will actually hit that submit button with two tags selected to test out our new super powers. First, let’s update the view which displays a link so that it will also display any tags we assigned during the creation process. We simply add the following snippet wherever we want the tags to display and we will be all set.


@unless ($link->tags->isEmpty())
    <p>
       @foreach($link->tags as $tag)
          <span class="label label-danger">{{ $tag->name }}</span>
       @endforeach
    </p>
@endunless

We can see it is working like a charm!

belongsToMany With Tags


# Step 14: Make The Tag Select Box Pretty

We have a working solution at this point, but the select box as it looks right now is not really ideal. Let’s make use of a fantastic jQuery plugin called Select2 so that we can prettify the select box and turn it into a familiar tag select UI element. It’s incredibly easy to do. For this example, we simply added the following snippets to our master layout.

Place this snippet in the head section:

<link href="//cdnjs.cloudflare.com/ajax/libs/select2/4.0.0/css/select2.min.css" rel="stylesheet" />
<script src="//cdnjs.cloudflare.com/ajax/libs/select2/4.0.0/js/select2.min.js"></script>

Then just place this in the footer:


Finally, update the particular select element you are interested in to have an id which the select2 plugin can target. We used prettify as the id, so we can apply it to our select like so:

<div class="form-group">
{!! Form::label('tags', 'Choose at least one tag') !!}
{!! Form::select('tags[]', $tags, null, ['class' => 'form-control input-lg', 'multiple', 'id' => 'prettify', 'data-placeholder' => 'Choose at least one tag']) !!}
</div>

Of course this is simply a quick and dirty way to get this running with minimal effort. It may be best to use Gulp and Elixir when actually deploying this to production to handle asset compilation and minification.

With this simple addition, we can see that our tags now look ridiculously awesome!
select2 tags

select2 tags display

More Helpful Resources

The following links apply directly to this episode, and should be very helpful to you.
http://laravel.com/-docs/master/eloquent-relationships#many-to-many
http://laravel.com/-docs/master/eloquent#defining-models
https://laracasts.com/-series/laravel-5-fundamentals

In addition, it may be helpful to have a look at the source code of the relevant methods we used in this tutorial.

lists()

public function lists($column, $key = null)
{
	$results = $this->query->lists($column, $key);

	// If the model has a mutator for the requested column, we will spin through
	// the results and mutate the values so that the mutated version of these
	// columns are returned as you would expect from these Eloquent models.
	if ($this->model->hasGetMutator($column)) {
		foreach ($results as $key => &$value) {
			$fill = [$column => $value];

			$value = $this->model->newFromBuilder($fill)->$column;
		}
	}

	return collect($results);
}

attach()

public function attach($id, array $attributes = [], $touch = true)
{
	if ($id instanceof Model) {
		$id = $id->getKey();
	}

	$query = $this->newPivotStatement();

	$query->insert($this->createAttachRecords((array) $id, $attributes));

	if ($touch) {
		$this->touchIfTouching();
	}
}

sync()

public function sync($ids, $detaching = true)
{
	$changes = [
		'attached' => [], 'detached' => [], 'updated' => [],
	];

	if ($ids instanceof Collection) {
		$ids = $ids->modelKeys();
	}

	// First we need to attach any of the associated models that are not currently
	// in this joining table. We'll spin through the given IDs, checking to see
	// if they exist in the array of current ones, and if not we will insert.
	$current = $this->newPivotQuery()->lists($this->otherKey);

	$records = $this->formatSyncList($ids);

	$detach = array_diff($current, array_keys($records));

	// Next, we will take the differences of the currents and given IDs and detach
	// all of the entities that exist in the "current" array but are not in the
	// the array of the IDs given to the method which will complete the sync.
	if ($detaching && count($detach) > 0) {
		$this->detach($detach);

		$changes['detached'] = (array) array_map(function ($v) { return (int) $v; }, $detach);
	}

	// Now we are finally ready to attach the new records. Note that we'll disable
	// touching until after the entire operation is complete so we don't fire a
	// ton of touch operations until we are totally done syncing the records.
	$changes = array_merge(
		$changes, $this->attachNew($records, $current, false)
	);

	if (count($changes['attached']) || count($changes['updated'])) {
		$this->touchIfTouching();
	}

	return $changes;
}

Laravel belongsToMany Example Conclusion

In this episode we covered some really cool features of working with Laravel and setting up many to many relationships using the belongsToMany relationship. We now have a nice blueprint or playbook if you will for taking a step by step approach for adding many to many support for a project. Thanks for checking out the tutorial.

Click to share! ⬇️