Laravel-Metable Documentation¶
Laravel-Metable is a package for easily attaching arbitrary data to Eloquent models for Laravel 5.
Introduction¶
Laravel-Metable is a package for easily attaching arbitrary data to Eloquent models for Laravel 5.
Features¶
- One-to-many polymorphic relationship allows attaching data to Eloquent models without needing to adjust the database schema.
- Type conversion system allows data of numerous different scalar and object types to be stored, queried and retrieved. See the list of supported Data Types.
Installation¶
- Add the package to your Laravel app using composer
composer require plank/laravel-metable
- Register the package’s service provider in
config/app.php
. In Laravel versions 5.5 and beyond, this step can be skipped if package auto-discovery is enabled.
<?php
'providers' => [
...
Plank\Metable\MetableServiceProvider::class,
...
];
- Publish the config file (
config/metable.php
) of the package using artisan.
php artisan vendor:publish --provider="Plank\Metable\MetableServiceProvider"
- Run the migrations to add the required table to your database.
php artisan migrate
- Add the Plank\Metable\Metable trait to any eloquent model class that you want to be able to attach metadata to.
Example Usage¶
Attach some metadata to an eloquent model
<?php
$post = Post::create($this->request->input());
$post->setMeta('color', 'blue');
Query the model by its metadata
<?php
$post = Post::whereMeta('color', 'blue');
Retrieve the metadata from a model
<?php
$value = $post->getMeta('color');
Handling Meta¶
before you can attach meta to an Eloquent model, you must first add the Metable
trait to your Eloquent model.
<?php
namespace App;
use Plank\Metable\Metable;
use Illuminate\Database\Eloquent\Model;
class Page extends Model
{
use Metable;
// ...
}
Note
The Metable trait adds a meta()
relationship to the model. However, it also keeps meta keys indexed separately to speed up reads. As such, it is recommended to not modify this relationship directly and to instead only use the methods described in this document.
Attaching Meta¶
Attach meta to a model with the setMeta()
method. The method accepts two arguments: a string to use as a key and a value. The value argument will accept a number of different inputs, which will be converted to a string for storage in the database. See the list of a supported Data Types.
<?php
$model->setMeta('key', 'value');
To set multiple meta key and value pairs at once, you can pass an associative array or collection to setManyMeta()
. The meta will be added to the model.
<?php
$model->setManyMeta([
'name' => 'John Doe',
'age' => 18,
]);
To replace existing meta with a new set of meta, you can pass an associative array or collection to syncMeta()
. All existing meta will be removed before the new meta is attached.
<?php
$model->syncMeta([
'name' => 'John Doe',
'age' => 18,
]);
Retrieving Meta¶
You can retrieve the value of the meta at a given key with the getMeta()
method. The value should be returned in the same format that it was stored. For example, if an array is set, you will receive an array back when retrieving it.
<?php
$model->setMeta('age', 18);
$model->setMeta('approved', true);
$model->setMeta('accessed_at', Carbon::now());
//reload the model from the database
$model = $model->fresh();
$age = $model->getMeta('age'); //returns an integer
$approved = $model->getMeta('approved'); //returns a boolean
$accessDate = $model->getMeta('accessed_at'); //returns a Carbon instance
//etc.
Once loaded, all meta attached to a model instance are cached in the model’s meta
relationship. As such, successive calls to getMeta()
will not hit the database repeatedly.
Similarly, the unserialized value of each meta is cached once accessed. This is particularly relevant for attached Eloquent Models and similar database-dependant objects.
Setting a new value for a key automatically updates all caches.
Default Values¶
You may pass a second parameter to the getMeta()
method in order to specify a default value to return if no meta has been set at that key.
<?php
$model->getMeta('status', 'draft'); // will return 'draft' if not set
Alternatively, you may set default values as key-value pairs on the model itself, instead of specifying them at each individual call site. If a default has been defined from this property and a value is also passed as to the default parameter, the parameter will take precedence.
<?php
class ExampleMetable extends Model {
use Metable;
protected $defaultMetaValues = [
'color' => '#000000'
];
//...
}
<?php
$model->getMeta('color'); // will return '#000000' if not set
$model->getMeta('color', null); // will return null if not set
$model->getMeta('color', '#ffffff'); // will return '#ffffff' if not set
Note
If a falsey value (e.g. 0
, false
, null
, ''
) has been manually set for the key, that value will be returned instead of the default value. The default value will only be returned if no meta exists at the key.
Retrieving All Meta¶
To retrieve a collection of all meta attached to a model, expressed as key and value pairs, use getAllMeta()
.
<?php
$meta = $model->getAllMeta();
Checking For Presence of Meta¶
You can check if a value has been assigned to a given key with the hasMeta()
method.
<?php
if ($model->hasMeta('background-color')) {
// ...
}
Note
This method will return true
even if a falsey value (e.g. 0
, false
, null
, ''
) has been manually set for the key.
Deleting Meta¶
To remove the meta stored at a given key, use removeMeta()
.
<?php
$model->removeMeta('preferred_language');
To remove multiple meta at once, you can pass an array of keys to removeManyMeta()
.
<?php
$model->removeManyMeta([
'preferred_language',
'store_currency',
'user_timezone',
]);
To remove all meta from a model, use purgeMeta()
.
<?php
$model->purgeMeta();
Attached meta is automatically purged from the database when a Metable
model is manually deleted. Meta will not be cascaded if the model is deleted by the query builder.
<?php
$model->delete(); // will delete attached meta
MyModel::where(...)->delete() // will NOT delete attached meta
Eager Loading Meta¶
When working with collections of Metable
models, be sure to eager load the meta relation for all instances together to avoid repeated database queries (i.e. N+1 problem).
Eager load from the query builder:
<?php
$models = MyModel::with('meta')->where(...)->get();
Lazy eager load from an Eloquent collection:
<?php
$models->load('meta');
You can also instruct your model class to always eager load the meta relationship by adding 'meta'
to your model’s $with
property.
<?php
class MyModel extends Model {
use Metable;
protected $with = ['meta'];
}
Querying Meta¶
The Metable
trait provides a number of query scopes to facilitate modifying queries based on the meta attached to your models
Checking for Presence of a key¶
To only return records that have a value assigned to a particular key, you can use whereHasMeta()
. You can also pass an array to this method, which will cause the query to return any models attached to one or more of the provided keys.
<?php
$models = MyModel::whereHasMeta('notes')->get();
$models = MyModel::whereHasMeta(['queued_at', 'sent_at'])->get();
If you would like to restrict your query to only return models with meta for all of the provided keys, you can use whereHasMetaKeys()
.
<?php
$models = MyModel::whereHasMetaKeys(['step1', 'step2', 'step3'])->get();
You can also query for records that does not contain a meta key using the whereDoesntHaveMeta()
. Its signature is identical to that of whereHasMeta()
.
<?php
$models = MyModel::whereDoesntHaveMeta('notes')->get();
$models = MyModel::whereDoesntHaveMeta(['queued_at', 'sent_at'])->get();
Comparing value¶
You can restrict your query based on the value stored at a meta key. The whereMeta()
method can be used to compare the value using any of the operators accepted by the Laravel query builder’s where()
method.
<?php
// omit the operator (defaults to '=')
$models = MyModel::whereMeta('letters', ['a', 'b', 'c'])->get();
// greater than
$models = MyModel::whereMeta('name', '>', 'M')->get();
// like
$models = MyModel::whereMeta('summary', 'like', '%bacon%')->get();
//etc.
The whereMetaIn()
method is also available to find records where the value is matches one of a predefined set of options.
<?php
$models = MyModel::whereMetaIn('country', ['CAN', 'USA', 'MEX']);
The whereMeta()
and whereMetaIn()
methods perform string comparison (lexicographic ordering). Any non-string values passed to these methods will be serialized to a string. This is useful for evaluating equality (=
) or inequality (<>
), but may behave unpredictably with some other operators for non-string data types.
<?php
// array value will be serialized before it is passed to the database
$model->setMeta('letters', ['a', 'b', 'c']);
// array argument will be serialized using the same mechanism
// the original model will be found.
$model = MyModel::whereMeta('letters', ['a', 'b', 'c'])->first();
Depending on the format of the original data, it may be possible to compare against subsets of the data using the SQL like
operator and a string argument.
<?php
$model->setMeta('letters', ['a', 'b', 'c']);
// check for the presence of one value within the json encoded array
// the original model will be found
$model = MyModel::whereMeta('letters', 'like', '%"b"%' )->first();
When comparing integer or float values with the <
, <=
, >=
or >
operators, use the whereMetaNumeric()
method. This will cast the values to a number before performing the comparison, in order to avoid common pitfalls of lexicographic ordering (e.g. '11'
is greater than '100'
).
<?php
$models = MyModel::whereMetaNumeric('counter', '>', 42)->get();
Ordering results¶
You can apply an order by clause to the query to sort the results by the value of a meta key.
<?php
// order by string value
$models = MyModel::orderByMeta('nickname', 'asc')->get();
//order by numeric value
$models = MyModel::orderByMetaNumeric('score', 'desc')->get();
By default, all records matching the rest of the query will be ordered. Any records which have no meta assigned to the key being sorted on will be considered to have a value of null
.
To automatically exclude all records that do not have meta assigned to the sorted key, pass true
as the third argument. This will perform an inner join instead of a left join when sorting.
<?php
// sort by score, excluding models which have no score
$model = MyModel::orderByMetaNumeric('score', 'desc', true)->get();
//equivalent to, but more efficient than
$models = MyModel::whereHasMeta('score')
->orderByMetaNumeric('score', 'desc')->get();
A Note on Optimization¶
Laravel-Metable is intended a convenient means for handling data of many different shapes and sizes. It was designed for dealing with data that only a subset of all models in a table would have any need for.
For example, you have a Page model with a template field and each template needs some number of additional fields to modify how it displays. If you have X templates which each have up to Y fields, adding all of these as columns to pages table will quickly get out of hand. Instead, appending these template fields to the Page model as meta can make handling this use case trivial.
Laravel-Metable makes it very easy to append just about any data to your models. However, for sufficiently large data sets or data that is queried very frequently, it will often be more efficient to use regular database columns instead in order to take advantage of native SQL data types and indexes. The optimal solution will depend on your use case.
Data Types¶
You can attach a number of different kinds of values to a Metable
model. The data types that are supported by Laravel-Mediable out of the box are the following.
Scalar Values¶
The following scalar values are supported.
Array¶
Arrays of scalar values. Nested arrays are supported.
<?php
$metable->setMeta('information', [
'address' => [
'street' => '123 Somewhere Ave.',
'city' => 'Somewhereville',
'country' => 'Somewhereland',
'postal' => '123456',
],
'contact' => [
'phone' => '555-555-5555',
'email' => 'email@example.com'
]
]);
Warning
Laravel-Metable uses json_encode()
and json_decode()
under the hood for array serialization. This will cause any objects nested within the array to be cast to an array.
Boolean¶
<?php
$metable->setMeta('accepted_promotion', true);
Integer¶
<?php
$metable->setMeta('likes', 9001);
Float¶
<?php
$metable->setMeta('precision', 0.755);
Null¶
<?php
$metable->setMeta('linked_model', null);
String¶
<?php
$metable->setMeta('attachment', '/var/www/html/public/attachment.pdf');
Objects¶
The following classes and interfaces are supported.
Eloquent Models¶
It is possible to attach another Eloquent model to a Metable
model.
<?php
$page = App\Page::where(['title' => 'Welcome'])->first();
$metable->setMeta('linked_model', $page);
When $metable->getMeta()
is called, the attached model will be reloaded from the database.
It is also possible to attach a Model
instance that has not been saved to the database.
<?php
$metable->setMeta('related', new App\Page);
When $metable->getMeta()
is called, a fresh instance of the class will be created (will not include any attributes).
Eloquent Collections¶
Similarly, it is possible to attach multiple models to a key by providing an instance of Illuminate\Database\Eloquent\Collection
containing the models.
As with individual models, both existing and unsaved instances can be stored.
<?php
$users = App\User::where(['title' => 'developer'])->get();
$metable->setMeta('authorized', $users);
DateTime & Carbon¶
Any object implementing the DateTimeInterface
. Object will be converted to a Carbon
instance.
<?php
$metable->setMeta('last_viewed', \Carbon\Carbon::now());
Serializable¶
Any object implementing the PHP Serializable
interface.
<?php
class Example implements \Serializable
{
//...
}
$serializable = new Example;
$metable->setMeta('example', $serializable);
Plain Objects¶
Any other objects will be converted to stdClass
plain objects. You can control what properties are stored by implementing the JsonSerializable
interface on the class of your stored object.
<?php
$metable->setMeta('weight', new Weight(10, 'kg'));
$weight = $metable->getMeta('weight') // stdClass($amount = 10; $unit => 'kg');
Note
The Plank\Metable\DataType\ObjectHandler
class should always be the last entry the config/metable.php
datatypes array, as it will accept any object, causing any handlers below it to be ignored.
Warning
Laravel-Metable uses json_encode()
and json_decode()
under the hood for plain object serialization. This will cause any arrays within the object’s properties to be cast to a stdClass
object.
Adding Custom Data Types¶
You can add support for other data types by creating a new Handler
for your class, which can take care of serialization. Only objects which can be converted to a string and then rebuilt from that string should be handled.
Define a class which implements the Plank\Metable\DataType\Handler interface and register it to the 'datatypes'
array in config/metable.php
. The order of the handlers in the array is important, as Laravel-Metable will iterate through them and use the first entry that returns true
for the canHandleValue()
method for a given value. Make sure more concrete classes come before more abstract ones.
Extending Meta¶
Here are some mechanisms provided to customize the behaviour of this package.
Adjusting Schema¶
If you wish to modify the database schema of the package, it is recommended that you copy the base migration files provided by this package into your application’s database/migration folder. If the file names match exactly, the ones provided by this package will be ignored.
If the customization that you are applying would cause conflicts with future migrations (e.g. changing table name), then it is recommended to set the metable.applyMigrations
config to false
, which will disable future migrations from being run. Do note that you may need to apply migrations provided in future major versions of the package manually to avoid conflicts.
Adjusting the Model¶
You can modify the Meta model by simply extending the class.
If you wish to use the same custom Meta
subclass for all Metable
models, you can register the fully-qualified class name to the metable.model
config.
If you would prefer to use different Meta
subclasses for different entities (e.g. to keep data in separate tables), you can override the Metable::getMetaClassName()
method of each model to specify the desired Meta
class to use for each entity.
<?php
class UserMeta extends Meta
{
protected $table = 'user_meta';
}
class ProductMeta extends Meta
{
protected $table = 'product_meta';
}
class User extends Model
{
use Metable;
protected function getMetaClassName(): string
{
return UserMeta::class;
}
}
class Product extends Model
{
use Metable;
protected function getMetaClassName(): string
{
return ProductMeta::class;
}
}