Laravel Eloquent collection of mixed models - laravel

I have a database with a table called files. Within that, we have the following structure -
- id
- parent_id (nullable)
- name
- type (enum: File or Folder)
- created_at
- updated_at
I then have two models, one called File and one called Folder. Folder extends File. Is there a way that when I call File::all(), for example, I can utilize Eloquent to map the respective models based on the databases type field?

Eloquent returns collection instances, so one way would be to call map() and have that return the appropriate objects for each item, eg, if it's a file just return the file, whereas if it's a folder populate a new Folder instance and return it.
Or you could have File and Folder be models that work off the same table, with global scopes used to limit the query set by type, then call all() on both of them and merge them.
But I think the best thing to do with them is make them a single model, that behaves differently based on the type. Put any differing functionality in methods on the model so it can be treated the same regardless of type by calling those methods. I think that's the safer option in that you're making your models polymorphic - they can be treated the same regardless of type.

I've been able to work out the answer by extending Laravel Models newFromBuilder method.
Here is my class -
class File {
public static $types = ['File', 'Folder'];
/**
* Create a new model instance that is existing.
*
* #param array $attributes
* #param null $connection
*
* #return Model|string
*/
public function newFromBuilder($attributes = [], $connection = null)
{
$model = $this->newInstanceFromType($attributes->type);
$model->exists = true;
$model->setRawAttributes((array) $attributes, true);
$model->setConnection($connection ?: $this->getConnectionName());
$model->fireModelEvent('retrieved', false);
return $model;
}
/**
* Determine our model instance based on the type field.
*
* #param string $type
*
* #return mixed
*/
private function newInstanceFromType(string $type)
{
if (!in_array($type, static::$types)) {
throw new InvalidArgumentException('$type must be one of static::$types');
}
$model = 'App\Models\\' . $type;
return new $model;
}
}
This will then return either a File or Folder model instance depending on what the type enum is from the database.
Thanks all for the input!

Related

Laravel - formatting all dates simultaneously

I'm currently working on a POC to showcase that it's going to be fairly painless to create an API with Laravel, the catch being that the database is already set in stone.
One problem I've run into is that they've used custom created at and updated at column names, e.g. for a car table, the created_at column would be car_time and the updated date would be cardata_time, and these are all saved as unix timestamps.
I know you can set the CREATED_AT and UPDATED_AT columns for each model. I want to go another step and return all dates in ISO 8601 format.
I've inserted a class between my models and Model called MasterModel and I want to do something like
protected function getCreatedAtAttribute($value)
{
$format = "Y-m-d\TH:i:s\Z";
$datetime = new DateTime($value);
return $datetime->format($format);
}
to make all created at dates be in that format. The problem is that I the custom created at and updated columns mean that this never gets called.
Is there a way for me to identify the created at and updated at columns in such a way that I can use a single method to updated all created at dates at the same time?
UPDATE: I realize my original question was not clear enough - I need to identify all fields that are dates, not just created_at and updated_at, and have them formatted a certain way. They will always be unix timestamps. Not sure how I'd go about this.
Here an answer that will expand on #caddy dz answer who happen to be sitting with me.
 All the things that need to be known
Deactivation of auto management of timestamps
public $timestamps = false; // <-- deactivate the automatic handling
Change table attributes names
const CREATED_AT = 'creation_date'; // <--- change the names
const UPDATED_AT = 'last_update';
source doc:
https://laravel.com/docs/5.8/eloquent#eloquent-model-conventions
By default, Eloquent expects created_at and updated_at columns to
exist on your tables. If you do not wish to have these columns
automatically managed by Eloquent, set the $timestamps property on
your model to false:
 Creating the accessors
class User extends Model
{
/**
* Get the user's first name.
*
* #param string $value
* #return string
*/
public function getFirstNameAttribute($value)
{
// do whatever you want here (change and mutate the value)
return ucfirst($value);
}
}
First thing to know, is that the accessors are a global concept for
eloquent and can be writing for all attributes and not just
getCreatedAtAttribute or getUpdatedAtAttribute.
Second thing to know is that whatever the name of the column, that is
in camle case (firstName) or with _ (first_name) eloquent know to
match to it. The format of the accessor should be
get[NameOfATtribute]Attribute in pascal case (camle case but first
letter too in uppercase).
Three the method argument hold the value of the column in
question. Bellow a snippet that show how it's used
$user = App\User::find(1);
$firstName = $user->first_name; //|=> first_name => getFirstNameAttribute(columnVal)
The resolution is clear.
first_name (column name) => getFirstNameAttribute(columnValue)
All the snippets are from the doc: https://laravel.com/docs/5.8/eloquent-mutators#accessors-and-mutators
 Let's apply all of that
First we need to not use $table->timestamps() in the migration so we make the changment to the bellow.
Schema::create('cars', function (Blueprint $table) {
$table->bigIncrements('id');
$table->timestamp('cardata_time', 0)->nullable();
$table->timestamp('car_time', 0)->nullable();
});
Then we apply the modification on our model:
- we deactivate the auto handling of timestamps.
- Override the timestamps columns names.
- And create the accessors.
Here depend on what we want. If we want to only do the above here a snippet that show that:
// deactivate auto timestamps management
public $timestamps = false;
// change the columns names
const CREATED_AT = 'car_time';
const UPDATED_AT = 'cardata_time';
// creating the accessors (respect the naming)
protected function getCarTimeAttribute($value) //car_time => CarTime
{
// <-- do whatever you want here (example bellow)
$format = "Y-m-d\TH:i:s\Z";
$datetime = new DateTime($value);
return $datetime->format($format);
}
protected function getCardataTimeAttribute($value) //cardata_time => CardataTime
{
// <-- do whatever you want here
$format = "Y-m-d\TH:i:s\Z";
$datetime = new DateTime($value);
return $datetime->format($format);
}
 Doing it with renaming the attributes completely
If what you want is to use another accessing name. Then what my friend #caddy dz did is the way to go. Which happen to be sitting with me. And dared me to expand upon the answer. (hhhh)
You will need to know
$appends and $hidden
Part of the serialization API.
https://laravel.com/docs/master/eloquent-serialization#appending-values-to-json
https://laravel.com/docs/master/eloquent-serialization#hiding-attributes-from-json
$appends allow us to add attributes to the model. That don't exists on the table. We need also to create an accessors for them.
class User extends Model
{
/**
* The accessors to append to the model's array form.
*
* #var array
*/
protected $appends = ['is_admin'];
// ........
/**
* Get the administrator flag for the user.
*
* #return bool
*/
public function getIsAdminAttribute()
{
return $this->attributes['admin'] == 'yes';
}
}
and
$hidden allow us to remove and limit the attribute from the models. Like with the password field.
Doc examples:
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
class User extends Model
{
/**
* The attributes that should be hidden for arrays.
*
* #var array
*/
protected $hidden = ['password'];
}
And from that what we need to do is to hide the attributes holding the time, that want to be changed to somehting else.
// remove the old attributes names
protected $hidden = ['car_time', 'cardata_time']; // renaming those
// append the new one \/ \/ <- to those
protected $appends = ['car_crated_at', 'cardata_created_at']; // names just for illustration
protected function getCarCreatedAtAttribute($value) // car_created_at => CarCreatedAt
{
$format = "Y-m-d\TH:i:s\Z";
$datetime = new DateTime($value);
return $datetime->format($format);
}
protected function getCardataCreatedAtAttribute($value) // cardata_created_at => CardataCreatedAt
{
$format = "Y-m-d\TH:i:s\Z";
$datetime = new DateTime($value);
return $datetime->format($format);
}
 Applying it for different models
The basic idea is to create a base model then extend it when you create your model.
Formatting all time attributes of the model without exception
If what you want is to apply the formatting for all the time attributes within the model.
Then override serializeDate() method. In practice write a trait, and then you can apply it. Otherwise a base model.
The answer bellow cover it well:
https://stackoverflow.com/a/41569026/7668448
And historically This thread is interesting :
https://github.com/laravel/framework/issues/21703
Serializing in Carbon level
In the documentation laravel 5.7 and up (what i checked [doc only]) :
https://laravel.com/docs/master/eloquent-serialization#date-serialization
We can change the formatting at the level of carbon serialization. But it happen that there was a bug in the past. Normally fixed but i didn't try it. Bug was in 5.7 and fixed in 5.7 if i'm not wrong. The git link above discuss it.
Snippet:
class AppServiceProvider extends ServiceProvider
{
/**
* Perform post-registration booting of services.
*
* #return void
*/
public function boot()
{
Carbon::serializeUsing(function ($carbon) {
return $carbon->format('U');
});
}
___THE_END ^ ^
Not sure what you're asking but if you have cardata_time and car_time in your table defined like this
Schema::create('cars', function (Blueprint $table) {
$table->bigIncrements('id');
$table->timestamp('cardata_time', 0)->nullable();
$table->timestamp('car_time', 0)->nullable();
});
And a MasterModel like so
/**
* Indicates if the model should be timestamped.
*
* #var bool
*/
public $timestamps = false;
const CREATED_AT = 'created_at';
const UPDATED_AT = 'updated_at';
/**
* The accessors to append to the model's array form.
*
* #var array
*/
protected $appends = ['created_at', 'updated_at'];
/**
* The attributes that should be hidden for arrays.
*
* #var array
*/
protected $hidden = ['car_time', 'cardata_time'];
protected function getCreatedAtAttribute($value)
{
$format = "Y-m-d\TH:i:s\Z";
$datetime = new DateTime($value);
return $datetime->format($format);
}
protected function getUpdatedAtAttribute($value)
{
$format = "Y-m-d\TH:i:s\Z";
$datetime = new DateTime($value);
return $datetime->format($format);
}
Results:
{
"id": 1,
"created_at": "2019-09-02T20:31:38Z",
"updated_at": "2019-09-02T20:31:38Z"
}
As in the documentation. The first approach. This requires the dates, to be defined in the $dates property. This will only be triggered if the Model is serialized.
public class YourModel extends Model
{
protected $dateFormat = "Y-m-d\TH:i:s\Z";
}
You can also define it in a provider in the boot method. Which will trigger when a Carbon date is serialized.
public function boot()
{
Carbon::serializeUsing(function ($carbon) {
return $carbon->format("Y-m-d\TH:i:s\Z");
});
}

Using toHasOne macro with MorpMany relationship

I have multiple Model classes that utilize a HasRetirements trait class. Both models use a MorphMany relationship to target the associated retirements table model for each model. Inside the HasRetirements trait class, I also have a isRetired() method as well as a currentRetirement() method. These methods are shown below.
I have come across a macro that can be chained onto an Eloquent relationship so that you can retrieve a single record. The macro toHasOne() utilizes model relationships through a hasMany relationship however my question is could this also be used for a morphMany relationship since it's polymorphic.
https://scotch.io/tutorials/understanding-and-using-laravel-eloquent-macros
public function currentRetirement()
{
return $this->retirements()->whereNull('ended_at')->latest()->toHasOne();
}
public function isRetired()
{
return $this->retirements()->whereNull('ended_at')->exists();
}
With Laravel 5.5, you could register a macro returning a derived class from the BelongsToMany relation. This derived class also could be an anonymous class if you are not planning on using it anywhere else. Within the derived class, you need to override the match method and return the single object as a relation or null otherwise
BelongsToMany::macro('asSingleEntity', function() {
return new class(
$this->related->newQuery(),
$this->parent,
$this->table,
$this->foreignPivotKey,
$this->relatedPivotKey,
$this->parentKey,
$this->relatedKey,
$this->relationName) extends BelongsToMany {
/**
* Match the eagerly loaded results to their parents.
*
* #param array $models
* #param \Illuminate\Database\Eloquent\Collection $results
* #param string $relation
* #return array
*/
public function match(array $models, Collection $results, $relation)
{
$dictionary = $this->buildDictionary($results);
// Once we have an array dictionary of child objects we can easily match the
// children back to their parent using the dictionary and the keys on the
// the parent models. Then we will return the hydrated models back out.
foreach ($models as $model) {
if (isset($dictionary[$key = $model->{$this->parentKey}])) {
$model->setRelation(
// $relation, $this->related->newCollection($dictionary[$key]) // original code
$relation, array_first($dictionary[$key])
);
} else {
$model->setRelation($relation, null);
}
}
return $models;
}
};
});
Then, you could simply use it within the model.
return $this
->belongsToMany(\App\Models\Entity::class, 'pivot_table_name')
->asSingleEntity();

Laravel Model conditional formatting

I have a database and model called Vote_actions that looks like this:
id
group_id
user_id
action_type
anonymous (boolean)
User can ask to be anonymous (that would make the boolean value to be true).If that is the case, I want to change the group_id and user_id from the returned model to -1.
Is there a way in laravel that I can do it ?
I know this question is old. I was looking for a way to hide some fields on certain conditions, external conditions like Auth Roles, and internal conditions like Model attributes, and I found a very flexible way to hide them.
And since I saw the other OP's duplicated post Laravel Hidden Fields On Condition asking for hiding field instead, So I'm gonna share it with you.
I know a mutator can change the value of its field, but to Hide it, you need :
the $hidden array attribute
the constructor __Construct() (optional)
to override method newFromBuilder method of Laravel Model
Here are the processes in the Model app\Vote_actions.php:
Hidden. Let's say you normally want to hide the fields created_at and updated_at of Laravel, you use:
protected $hidden = ['created_at', 'updated_at'];
External Conditions. Now let's say if the Authenticated User is Staff you want to unhide them:
public function __Construct()
{
parent::__construct();
if(\Auth::check() && \Auth::user()->isStaff()) {
// remove all fields so Staff can access everything for example
$this->hidden = [];
} else {
// let's hide action_type for Guest for example
$this->hidden = array_merge($this->hidden, ['action_type'];
}
}
Internal Conditions Let's say now you want to hide anonymous field is its value is true:
/**
* Create a new model instance that is existing.
*
* #param array $attributes
* #param array $connection
* #return \Illuminate\Database\Eloquent\Model|static
*/
public function newFromBuilder($attributes = array(), $connection = null)
{
$instance = parent::newFromBuilder($attributes, $connection);
if((bool)$instance->anonymous === true) {
// hide it if array was already empty
// $instance->hidden = ['anonymous'];
// OR BETTER hide single field with makeHidden method
$instance->makeHidden('anonymous');
// the opposite is makeVisible method
}
return $instance;
}
You can't play with hidden attributes and method inside mutators, that's their weakness when we need to hide instead of changing values.
But in any case, understand that calling modification on high load of hundredths of rows can be costly in time.
You are leaning towards an edge case, with special conditions.
Make use of accessors:
class VoteActions extends \Eloquent {
public $casts = [
'anonymous' => 'boolean'
];
...
/**
* Accessors: Group ID
* #return int
*/
public function getGroupIdAttribute()
{
if((bool)$this->anonymous === true) {
return -1;
} else {
return $this->group_id;
}
}
/**
* Accessors: User ID
* #return int
*/
public function getUserIdAttribute()
{
if((bool)$this->anonymous === true) {
return -1;
} else {
return $this->user_id;
}
}
}
Official Documentation: https://laravel.com/docs/5.1/eloquent-mutators#accessors-and-mutators
However, i would recommend that you set the value in the database directly to -1 where necessary so as to preserve the integrity of your application.
Of course you can easily do that. Read about accessors (getters):
https://laravel.com/docs/5.1/eloquent-mutators
Example:
function getUserIdAttribute()
{
return $this->anonymous ? -1 : $this->user_id;
}
function getGroupIdAttribute()
{
return $this->anonymous ? -1 : $this->group_id;
}

Laravel 5.1 Reusable Slug

Hello how can I make the unique slug logic reusable in my project. I have a method for creating a unique slug in my Product model and I want to use that same logic in my other model here:
/**
* Set the name attribute and automatically the slug
*
* #param string $name
*/
public function setNameAttribute($name)
{
$this->attributes['name'] = $name;
if(! $this->exists)
{
$this->setUniqueSlug($name, '');
}
}
/**
* Recursive routine to set a unique slug
*
* #param string $name
* #param mixed $extra
*/
public function setUniqueSlug($name, $extra)
{
$slug = str_slug($name . '-' . $extra);
if (static::whereSlug($slug)->exists())
{
$this->setUniqueSlug($name, $extra + 1);
return;
}
$this->attributes['slug'] = $slug;
}
Martin Beans comments would be the best way to go about this.
There is nothing wrong with Paul Vidal's way, however, I wouldn't use the Base Model approach unless you're going to use it with every model.
As long as you're not using different field names for the slug and you're using something like a parent slug (i.e. product/{slug}) you should be able to put the above code straight into a trait and then use in on the models you want to.
Hope this helps!
There are many ways, you can create a Base model class that extends from eloquent, and then extends the rest of your models from that base model. Create the method "setUniqueSlug" in that base model to be accesible from any other.
Or you can create a Helper class, so you can call "setUniqueSlug" from anywhere.

Dynamically hide certain columns when returning an Eloquent object as JSON?

How do dynamically hide certain columns when returning an Eloquent object as JSON? E.g. to hide the 'password' column:
$users = User::all();
return Response::json($users);
I'm aware I can set protected properties in the model ($hidden or $visible), but how do I set these dynamically? I might want to hide or show different columns in different contexts.
$model->getHidden();
$model->setHidden(array $columns);
$model->setVisible(array $columns);
From Lavarel 5.3 Documentation :
Temporarily Modifying Attribute Visibility
If you would like to make some typically hidden attributes visible on a given model instance, you may use the makeVisible method. The makeVisible method returns the model instance for convenient method chaining:
return $user->makeVisible('attribute')->toArray();
Likewise, if you would like to make some typically visible attributes hidden on a given model instance, you may use the makeHidden method.
return $user->makeHidden('attribute')->toArray();
I've found a complete solution around the problem with using $model->setHidden(array $columns);
Lets say, for example, that you would like to decide in the controller exactly which fields to return. Updating only the model's hidden forces you to go over each model before you return an array of models for example. The problem becomes even worse when those models have relationships that you would also like to change. You have to loop over each model, set the hidden attribute, and then for each also set the relationships hidden. What a mess.
My solution involves creating a static member for each model that when present, updates the visible/hidden attribute just before the call to "toArray":
<?php
trait DynamicHiddenVisible {
public static $_hidden = null;
public static $_visible = null;
public static function setStaticHidden(array $value) {
self::$_hidden = $value;
return self::$_hidden;
}
public static function getStaticHidden() {
return self::$_hidden;
}
public static function setStaticVisible(array $value) {
self::$_visible = $value;
return self::$_visible;
}
public static function getStaticVisible() {
return self::$_visible;
}
public static function getDefaultHidden() {
return with(new static)->getHidden();
}
public static function geDefaultVisible() {
return with(new static)->getVisible();
}
public function toArray() {
if (self::getStaticVisible())
$this->visible = self::getStaticVisible();
else if (self::getStaticHidden())
$this->hidden = self::getStaticHidden();
return parent::toArray();
}
}
As an added bonus, I expose a way to the model's default hidden/visible that you may have set in your model's class.
Don't to forget to add the trait
class Client extends Eloquent {
use DynamicHiddenVisible;
}
Finally, in the controller, before returning your model, decide on visible/hidden attributes:
public function getIndex($clientId) {
// in this specific call, I would like to hide the "special_type" field of my Client model
$hiddenFields = Client::getDefaultHidden();
array_push($hiddenFields, "special_type");
Client::setStaticHidden($hiddenFields);
return Client::find($clientId)->toJson();
}
I don't believe it is the job of the ORM to worry about presentation logic, and that is what JSON is. You'll aways need to cast data to various types as well as hide things and sometimes create a buffer zone to rename things safely.
You can do all of that with Fractal which I built for exactly this reason.
<?php namespace App\Transformer;
use Acme\Model\Book;
use League\Fractal\TransformerAbstract;
class BookTransformer extends TransformerAbstract
{
/**
* List of resources possible to include
*
* #var array
*/
protected $availableIncludes = [
'author'
];
/**
* Turn this item object into a generic array
*
* #return array
*/
public function transform(Book $book)
{
return [
'id' => (int) $book->id,
'title' => $book->title,
'year' => (int) $book->yr,
'links' => [
[
'rel' => 'self',
'uri' => '/books/'.$book->id,
]
],
];
}
/**
* Include Author
*
* #return League\Fractal\ItemResource
*/
public function includeAuthor(Book $book)
{
$author = $book->author;
return $this->item($author, new AuthorTransformer);
}
}
Embedding (including) stuff might be a bit more than you need right now, but it can be very handy too.
In 5.4 you can hide and show attributes dinamically:
$model->makeVisible('attribute');
$model->makeHidden('attribute');
Laravel docs
In addition to #deczo's answer - I feel the $hidden variable is not really designed to be used dynamically. It is more to protect specific data from ever been incorrectly displayed (such as 'password').
If you want specific columns - you should probably just be using a select statement and just get the specific columns you want.
For Laravel 5.3 or greater version,
If you want to make multiple attributes temporary hidden or visible using single statement, you may use model->makeVisible() and model->makeHidden() methods with passing array of attributes.
For example, to hide multiple attributes,
$user->makeHidden(["attribute1", "attribute2", "attribute3"]);
And to make visible multiple attributes,
$user->makeVisible(["otherAttribute1", "otherAttribute2", "otherAttribute3"]);
In the Model:
protected $hidden = [
'your_field_1',
'your_field_2',
];
You can override the getHidden method in order to hide certain columns dynamically:
class FooModel extends Model
{
public function getHidden()
{
// do here your validations and return
// the columns names with the specific criteria
// you need
return ['columnName1', 'columnName2'];
}
}
Made a package for this that uses Model Policies.
https://github.com/salomoni/authorized-attributes
Use the Salomoni\AuthorizedAttributes trait
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
use Salomoni\AuthorizedAttributes;
class Post extends Model
{
use AuthorizedAttributes;
/**
* The attributes that should be hidden for serialization.
*
* #var array
*/
protected $hidden = ['author_comments'];
}
Create and register a model policy. Add methods for the hidden attributes in camel-case prefixed with see.
namespace App\Policies;
use App\User;
class PostPolicy
{
/**
* Determine if a post author_comments-atrribute can be seen by the user.
*
* #param \App\User $user
* #return bool
*/
public function seeAuthorComments(User $user)
{
return $user->isAuthor();
}
}

Resources