SkillAgentSearch skills...

Smoothie

Some fruity additions to Laravel's Eloquent!

Install / Use

/learn @michaelbaril/Smoothie
About this skill

Quality Score

0/100

Supported Platforms

Universal

README

Smoothie

Some fruity additions to Laravel's Eloquent:

:warning: Note: only MySQL is tested and actively supported.

Miscellaneous

Save model and restore modified attributes

This package adds a new option restore to the save method:

$model->save(['restore' => true]);

This forces the model to refresh its original array of attributes from the database before saving. It's useful when your database row has changed outside the current $model instance, and you need to make sure that the $model's current state will be saved exactly, even restoring attributes that haven't changed in the current instance:


$model1 = Article::find(1);
$model2 = Article::find(1);

$model2->title = 'new title';
$model2->save();

$model1->save(['restore' => true]); // the original title will be restored
                                    // because it hasn't changed in `$model1`

To use this option, you need your model to extend the Baril\Smoothie\Model class instead of Illuminate\Database\Eloquent\Model.

Update only

Laravel's native update method will not only update the provided fields, but also whatever properties of the model were previously modified:

$article = Article::create(['title' => 'old title']);
$article->title = 'new title';
$article->update(['subtitle' => 'new subtitle']);

$article->fresh()->title; // "new title"

This package provides another method called updateOnly, that will update the provided fields but leave the rest of the row alone:

$article = Article::create(['title' => 'old title']);
$article->title = 'new title';
$article->updateOnly(['subtitle' => 'new subtitle']);

$article->fresh()->title; // "old title"
$article->title; // "new title"
$article->subtitle; // "new subtitle"

To use this method, you need your model to extend the Baril\Smoothie\Model class instead of Illuminate\Database\Eloquent\Model.

Explicitly order the query results

The package adds the following method to Eloquent collections:

$collection = YourModel::all()->sortByKeys([3, 4, 2]);

It allows for explicit ordering of collections by primary key. In the above example, the returned collection will contain (in this order):

  • model with id 3,
  • model with id 4,
  • model with id 2,
  • any other models of the original collection, in the same order as before calling sortByKeys.

Similarly, using the findInOrder method on models or query builders, instead of findMany, will preserve the order of the provided ids:

$collection = Article::findMany([4, 5, 3]); // we're not sure that the article
                                            // with id 4 will be the first of
                                            // the returned collection

$collection = Article::findInOrder([4, 5, 3]); // now we're sure

In order to use these methods, you need Smoothie's service provider to be registered in your config\app.php (or use package auto-discovery):

return [
    // ...
    'providers' => [
        Baril\Smoothie\SmoothieServiceProvider::class,
        // ...
    ],
];

Timestamp scopes

The Baril\Smoothie\Concerns\ScopesTimestamps trait provides some scopes for models with created_at and updated_at columns:

  • $query->orderByCreation($direction = 'asc'),
  • $query->createdAfter($date, $strict = false) (the $date argument can be of any datetime-castable type, and the $strict parameter can be set to true if you want to use a strict inequality),
  • $query->createdBefore($date, $strict = false),
  • $query->createdBetween($start, $end, $strictStart = false, $strictEnd = false),
  • $query->orderByUpdate($direction = 'asc'),
  • $query->updatedAfter($date, $strict = false),
  • $query->updatedBefore($date, $strict = false),
  • $query->updatedBetween($start, $end, $strictStart = false, $strictEnd = false).

Cross-database relations

With Laravel, it's possible to declare relations between models that don't belong to the same connection, but it will fail in some cases:

  • counting the relation and querying its existence won't work (because it uses a subquery),
  • many-to-many relations will work only when the pivot table is in the same database as the related model (because of the join).

This package provides a crossDatabase method that will solve this problem by prepending the table name with the database name. Of course, it works only if all databases are on the same server.

The usage is:

class Post
{
    public function comments()
    {
        return $this->hasMany(Comment::class)->crossDatabase();
    }

    public function category()
    {
        return $this->hasMany(Comment::class)->crossDatabase();
    }
}

For a many-to-many relation, you can specify whether the pivot table is in the same database as the parent table or the related table. In the example below, the pivot table is in the same database as the posts table:

class Post
{
    public function tags()
    {
        // same database as parent table (posts):
        return $this->belongsToMany(Tag::class)->crossDatabase('parent');
    }
}

class Tag
{
    public function posts()
    {
        // same database as related table (posts):
        return $this->belongsToMany(Post::class)->crossDatabase('related');
    }
}

Debugging

This package adds a debugSql method to the Builder class. It is similar as toSql except that it returns an actual SQL query where bindings have been replaced with their values.

Article::where('id', 5)->toSql(); // "SELECT articles WHERE id = ?" -- WTF?
Article::where('id', 5)->debugSql(); // "SELECT articles WHERE id = 5" -- much better

In order to use this method, you need Smoothie's service provider to be registered in your config\app.php (or use package auto-discovery).

(Credit for this method goes to Broutard, thanks!)

Field aliases

Basic usage

The Baril\Smoothie\Concerns\AliasesAttributes trait provides an easy way to normalize the attributes names of a model if you're working with an existing database with column namings you don't like.

There are 2 different ways to define aliases:

  • define a column prefix: all columns prefixed with it will become magically accessible as un-prefixed attributes,
  • define an explicit alias for a given column.

Let's say you're working with the following table (this example comes from the blog application Dotclear):

dc_blog
    blog_id
    blog_uid
    blog_creadt
    blog_upddt
    blog_url
    blog_name
    blog_desc
    blog_status

Then you could define your model as follows:

class Blog extends Model
{
    const CREATED_AT = 'blog_creadt';
    const UPDATED_AT = 'blog_upddt';

    protected $primaryKey = 'blog_id';
    protected $keyType = 'string';

    protected $columnsPrefix = 'blog_';
    protected $aliases = [
        'description' => 'blog_desc',
    ];
}

Now the blog_id column can be simply accessed this way: $model->id. Same goes for all other columns prefixed with blog_.

Also, the blog_desc column can be accessed with the more explicit alias description.

The original namings are still available. This means that there are actually 3 different ways to access the blog_desc column:

  • $model->blog_desc (original column name),
  • $model->desc (because of the blog_ prefix),
  • $model->description (thanks to the explicit alias).

Note: you can't have an alias (explicit or implicit) for another alias. Aliases are for actual column names only.

Collisions and priorities

If an alias collides with a real column name, it will have priority over it. This means that in the example above, if the table had a column actually named desc or description, you wouldn't be able to access it any more. You still have the possibility to define another alias for the column though.

class Article
{
    protected $aliases = [
        'title' => 'meta_title',
        'original_title' => 'title',
    ];
}

In the example above, the title attribute of the model returns the value of the meta_title column in the database. The value of the title column can be accessed with the original_title attribute.

Also, explicit aliases have priority over aliases implicitely defined by a column prefix. This means that when an "implicit alias" collides with a real column name, you can define an explicit alias that restores the original column name:

class Article
{
    protected $aliases = [
        'title' => 'title',
    ];
    protected $columnsPrefix = 'a_';
}

Here, the title attribute of the model will return the value of the title column of the database. The a_title column can be accessed with the a_title attribute (or you can define another alias for it).

Accessors, casts and mutators

You can define accessors either on the original attribute name, or the alias, or both.

  • If there's an accessor on the original name only, it will always apply, whether you access the attribute with its original name or its alias.
  • If there's an accessor on the alias only, it will apply only if you access the attribute using its alias.
  • If there's an accessor on both, each will apply individually (and will receive the original $value).
class Blog extends Model
{
    const CREATED_AT = 'blog_creadt';
    const UPDATED_AT = 'blog_upddt';

    protected $primaryKey = 'blog_id';
    protected $keyTy
View on GitHub
GitHub Stars48
CategoryDevelopment
Updated1y ago
Forks3

Languages

PHP

Security Score

60/100

Audited on Feb 28, 2025

No findings