1

Ok, this is weird... You ready?

I have an item type on my site, lets call it SomeItem

It can have tags associated with it via a one-to-many relationship.

The sorts of queries that Laravel builds when dealing with tags for SomeItem are like this, for instance in response to route api/someitem/10:

select `tags`.*, `someitem_tag`.`someitem_id` as `pivot_someitem_id`, `someitem_tag`.`tag_id` as `pivot_tag_id` from `tags` inner join `someitem_tag` on `tags`.`id` = `someitem_tag`.`tag_id` where `someitem_tag`.`someitem_id` in (10)

When I create a second Item with identical settings - let's call it AnotherItems - it treats the database query for extracting tags in a different manner, using a different syntax in the queries. Extremely weird.

(and yes, I have an s at the end of the model name...)

For instance, this route api/anotheritems/1

produces this error:

Base table or view not found: 1146 Table 'mysite.tag_anotheritems' doesn't exist (SQL: select `tags`.*, `tag_anotheritems`.`anotheritems_id` as `pivot_anotheritems_id`, `tag_anotheritems`.`tag_id` as `pivot_tag_id` from `tags` inner join `tag_anotheritems` on `tags`.`id` = `tag_anotheritems`.`tag_id` where `tag_anotheritems`.`anotheritems_id` in (1))

See what is happening? Of course I am getting this error - in the database this tag table for AnotherItems is created as anotheritems_tag. That is analogous to SomeItem.

How on earth can Laravel be using syntax someitem_tag for one item but tag_anotheritems for another item??? WTF?

First let me show you how SomeItem is set up.

Here is the database structure related to Tags:

use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;

class CreateSomeItemTagTable extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('someitem_tag', function (Blueprint $table) {
            $table->integer('tag_id')->unsigned();
            $table->foreign('tag_id')->references('id')->on('tags')->onDelete('cascade');

            $table->integer('someitem_id')->unsigned();
            $table->foreign('someitem_id')->references('id')->on('someitems')->onDelete('cascade');

            $table->primary(array('tag_id', 'someitem_id'));
        });
    }

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

There is a Tags model/class that has this:

namespace App;

use Illuminate\Database\Eloquent\Model;

class Tag extends Model
{
    protected $fillable = ['name'];
    protected $hidden = [];
    public $timestamps = false;

    public function someitems()
    {
        return $this->belongsToMany(SomeItem::class);
    }

}

And here is some relevant lines for SomeItem model/class:

namespace App;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Builder;
use App\Presenters\Presentable;
use Illuminate\Notifications\Notifiable;
use Auth;

class Exercise extends Model
    implements Presentable
{
    use Traits\SerializesUniversalDate;
    use Traits\Presents;
    use Notifiable;

    protected $presenter = 'App\Presenters\SomeItemPresenter';
    protected $fillable = ['title', etc];
    protected $hidden = [];

    public function parentitem()
    {
        return $this->belongsTo(ParentItem::class);
    }

    public function tags()
    {
        return $this->belongsToMany(Tag::class);
    }

    /**
     * Update lesson tag array.
     *
     * @param  array    \App\Tag  $tags
     * @return void
     */
    public function updateTags($tagsArray)
    {
        foreach ($tagsArray as &$value)
        {
            $tag = Tag::where('name', $value['name'])->first();
            if (is_null($tag))
            {
                $tag = new Tag([
                    'name' => $value['name']
                ]);
                $tag->save();
            }
            if (!$this->tags->contains($tag->id))
            {
                $this->tags()->attach($tag->id);
            }
        }
        foreach($this->tags as &$existingTag)
        {
            if (!self::arrayContains($tagsArray, 'name', $existingTag->name))
            {
                $this->tags()->detach($existingTag->id);
            }
        }
        $this->load('tags');
    }


    private static function arrayContains($array, $key, $value)
    {
        foreach ($array as $item)
        {
            if($item[$key] == $value) return true;
        }
        return false;
    }

}

And here is some relevant code for SomeItem API controller:

namespace App\Http\Controllers\Api;

use Illuminate\Http\Request;

use App\Http\Requests;
use App\Http\Controllers\Controller;
use Illuminate\Support\Facades\Input;

class SomeItemController extends Controller
{
    public function index(Request $request)
    {
        $query = \App\SomeItem::query();

        return $query->get()->load('parentitem')->load('tags');
    }

    //show item for editing
    public function show($id)
    {

        $someitem = \App\SomeItem::find($id);

        $someitem->load('parentitem')->load('tags');
        $someitem->attachKindToFiles();
        return $someitem;


    }

    //store new entry to db

    public function store()
    {
        $someitem = \App\SomeItem::create(Input::all());
        isset(Input::all()['tags']) ? $someitem->updateTags(Input::all()['tags']) : '';
        return $someitem;
    }

    //update/save
    public function update($id)
    {
        $someitem = \App\SomeItem::find($id);
        $someitem->update(Input::all());
        $someitem->updateTags(Input::all()['tags']);
        $someitem->load('tags');
        return $someitem;
    }

There is also a SomeItem presenter and composer but they don't do anything with tags.

With AnotherItems, I literally I duplicated everything from SomeItem and just changed names as needed.

So in the Tag model there is

public function anotheritems()
    {
        return $this->belongsToMany(AnotherItems::class);
    }

In AnotherItems model there is this, for instance

    public function tags()
    {
        return $this->belongsToMany(Tag::class);
    }

In the AnotherItems API controller there is this, for instance (which is for route api/anotheritems/1):

 public function index(Request $request)
    {

        $query = \App\AnotherItems::query();

        if ($request->has('id')) {
            $query->where('id', $request['id']);
        }

        return $query->get()->load('parentitem')->load('tags');
    }

So, this is a total mystery. I have been trying to figure this out for 2 days now. And I continue asking myself

How on earth can Laravel be using syntax someitem_tag for one item but tag_anotheritems for another item???

I upgraded from laravel 5.2 to 5.3 and it is after the upgrade that I added this AnotherItems. But I can't figure out how that could possibly alter things in terms of these database queries.

I have tried a ton of artisan commands for clearing everything imaginable, but somewhere in the framework it wants to handle SomeItem and AnotherItems differently when building these join queries to extract/save tags.

Thoughts?

thanks, Brian


Decided to step through code in debugger. Seems things are breaking down in Str.php in various snake related function, and I also noticed a snakeCache call, whatever the heck that is. Not sure why such a strange methodology to determine table names... Also in these functions there is some pluralizing related checks, so maybe this is related to me using an s at the end of my item name. Pretty messed up stuff if an s at the end of a model name can cause two different logic branches...

Brian
  • 561
  • 5
  • 16
  • Ok, I changed the table name to `tag_anotheritems` and things are working, but this is really messed up and is not the solution I wanted to resort to. If the name of the model can cause Laravel to determine the name of a joining table in two different manners, based on, perhaps, pluralization vs not pluralization, then that means the logic is faulty. Maybe it is fixed in later versions of Laravel. But seriously, one should be able to call a model whatever the heck they want (within reason) and not worry that such naming can actually break something in the query builders! – Brian Sep 13 '19 at 07:52
  • [Specify your pivot table name in your relationship methods](https://stackoverflow.com/a/34897727/5878071) : `return $this->belongsToMany(AnotherItem::class, 'anotheritems_tag');` and the same for your `AnotherItem` class. – Kévin Bibollet Sep 13 '19 at 07:56
  • The thing is, I have some other one-to-many relationships with this new item and I just changed the table names and everything works. I didn't want to have to edit the code, since it is easier for me to keep track of table names than modifying code. But I think I will edit the laravel 5.3 source code so that this doesn't happen and just keep track of that edit. – Brian Sep 13 '19 at 08:05
  • If that's simpler for you, do it! Another note: reading your tag system, I think [polymorphic relationships](https://laravel.com/docs/5.3/eloquent-relationships#polymorphic-relations) could be a nice idea. :) – Kévin Bibollet Sep 13 '19 at 08:11

0 Answers0