Smoothie
Some fruity additions to Laravel's Eloquent!
Install / Use
/learn @michaelbaril/SmoothieREADME
Smoothie
Some fruity additions to Laravel's Eloquent:
- Miscellaneous
- Field aliases
- Accessor cache
- Fuzzy dates
- Mutually-belongs-to-many-selves relationship
- N-ary many-to-many relationships
- Dynamic relationships
- Orderable behavior
- Tree-like structures and closure tables
- Cacheable behavior
: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$dateargument can be of any datetime-castable type, and the$strictparameter can be set totrueif 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 theblog_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
