bwrigley's avatar

Eloquentish for next autoincrement ID?

Hello,

I have a Profile model that has a few fields which are indexable and therefore cannot be null and must be unique. So I'm auto-generating them at first save (can be edited later).

In Profile model:

    public function getNextId()
    {
        $query = DB::select("show table status like 'profiles'");
        return $query[0]->Auto_increment;

    }

In ProfileController:

        $profile = new Profile;
        $profileId = $profile->getNextId();
        $profile->first_name = $request->first_name;
        $profile->last_name = $request->last_name;
        $profile->long_title = $request->first_name . ' ' . $request->last_name . ' ' . $profileId;
        $profile->slug = strtolower($request->first_name . '_' . $request->last_name . '_' . $profileId);

        $user->profile()->save($profile);

All works great. Until I run my tests which are using SQLite for in-memory tests and then I get the error:

Illuminate\Database\QueryException: SQLSTATE[HY000]: General error: 1 near "show": syntax error (SQL: show table status like 'profiles')

So I'm guessing show is not SQLite syntax.

Is there a way to write my getNextId() method in a DB agnostic way, or do I need to find the current highest id and add 1?

0 likes
10 replies
Sti3bas's avatar
Sti3bas
Best Answer
Level 53

@bwrigley you should probably mock the method and return the static value. This would also make your test less brittle, because you're not relying on the value which is generated in the database:

use Illuminate\Support\Str;

//...

public function store(Profile $profile)
{
   $profile->fill([
      'first_name' => $request->first_name,
      'last_name' => $request->last_name,
      'long_title' => $request->first_name . ' ' . $request->last_name . ' ' . $profile->getNextId(),
      'slug' => Str::slug($profile->long_title),
   ]);

   Auth::user()->profile()->save($profile);
}

Test:

$this->mock(Profile::class, function ($mock) {
   $mock->makePartial();
   $mock->shouldReceive('newInstance')->andReturn($mock);
   $mock->shouldReceive('getNextId')->andReturn(1);
});

$this->post('/profiles', [
   'first_name' => 'Joe',
   'last_name' => 'Doe',
]);

//...

$this->assertEquals('Joe Doe 1', Auth::user()->profile->long_title);

Mocking Eloquent model is quite tricky, I've already wrote a detailed guide in this thread: https://laracasts.com/discuss/channels/testing/spying-method-inside-of-if-condition

Snapey's avatar

Anticipating the next insert value is really dangerous on a multi-user system. Don't do it.

1 like
bwrigley's avatar

@sti3bas thank you so much and thank you for your guide.

I am still hitting one issue though as I didn't show you the whole store method which now looks like this:

        $profile->fill([
            'first_name' => $request->first_name,
            'last_name' => $request->last_name,
            'long_title' => $request->first_name . ' ' . $request->last_name . ' ' . $profile->getNextId(),
            'slug' => Str::slug($profile->long_title)
         ]);

        $user->profile()->save($profile);

        $profile->emails()->create(['address' => $request->email]);

This last line is what causes the issue with

PDOException: SQLSTATE[HY000]: General error: 1 table emails has no column named mockery_2__app__profile_id

This is obviously because it is picking up the mocked field when trying to attach the two models. Any thoughts on how to get around this?

Thanks again!

Sti3bas's avatar

@bwrigley same as with table name, you will have to specify the foreign key for your relationship:

public function emails()
{
   return $this->hasMany('App\Email', 'profile_id');
}
bwrigley's avatar

@snapey

Yes I totally get that however what I am trying to do here is to ensure that the slug I am setting in this model is unique and can be edited by the user later.

The best way I can think of to ensure it's unique is to add the id to the slug.

So yes it's dangerous, but only if I get two users registering with the same name at exactly the same time.

bwrigley's avatar

@sti3bas

Thank you, you are a star.

I only had

    public function emails(): HasMany
    {
        return $this->hasMany('App\Email');
    }
1 like
click's avatar

Yes I totally get that however what I am trying to do here is to ensure that the slug I am setting in this model is unique and can be edited by the user later.

Can't you use some other value than the ID? Like some random characters created by str_random(6) ?

Snapey's avatar

as @click, generate a random token or uuid. Something non-deterministic as opposed to the next id which has a higher chance of collision.

bwrigley's avatar

@click @snapey thanks for your thoughts.

Yes I may well do that, although less attractive if people choose not to edit their slug.

Thanks for input!

Please or to participate in this conversation.