In Laravel all models extend the base model.
The laravel eloquent models have a protected array attribute called $dates. Every date that is being added to this array is automatically converted to a Carbon instance.
I would like to extend the base model with similar functionality. For example with a protected $times attribute. All the time attributes would be converted to a Carbon instance
How would you do this?
Thanks in advance.
Thats easy whatever you want to do. Basic PHP knowledge.
If you want to add some other fields to be converted to Carbon instances, simply add them to $dates array
If you want to add some new parameters just extend laravel's Model as shown below
<?php
namespace App;
class MyModel extends \Illuminate\Database\Eloquent\Model
{
protected $awesomness = [];
/**
* override method
*
*/
public function getAttributeValue($key)
{
$value = $this->getAttributeFromArray($key);
// If the attribute has a get mutator, we will call that then return what
// it returns as the value, which is useful for transforming values on
// retrieval from the model to a form that is more useful for usage.
if ($this->hasGetMutator($key))
{
return $this->mutateAttribute($key, $value);
}
// If the attribute exists within the cast array, we will convert it to
// an appropriate native PHP type dependant upon the associated value
// given with the key in the pair. Dayle made this comment line up.
if ($this->hasCast($key))
{
return $this->castAttribute($key, $value);
}
// If the attribute is listed as a date, we will convert it to a DateTime
// instance on retrieval, which makes it quite convenient to work with
// date fields without having to create a mutator for each property.
if (in_array($key, $this->getDates()) && !is_null($value))
{
return $this->asDateTime($value);
}
//
//
// that's the important part of our modification
//
//
if (in_array($key, $this->awesomness) && !is_null($value))
{
return $this->doAwesomness($value);
}
return $value;
}
public function doAwesomness($value)
{
//do whatever you want here
return $value;
}
}
Then all of your models just need to extend \App\MyModel class instead of \Illuminate\Database\Eloquent\Model
Related
I have a method in my controller which basically is just storing a form to the database.
My form is very big and it has more than 30-40 fields.
So i need to store this information in different 3 tables on a condition.
For example :
foreach($request->all() as $answer):
if($answer->employeeType === 1){
//store data to type_one_table
} else if($answer->employeeType === 2){
//store data to type_two_table
} else if($answer->employeeType === 3){
//store data to type_two_table
} else {
//store data to some_other_table
}
endforeach;
So i was thinking to use some custom StoreInterface which will have some store method.
and extract above logic to its own implementation.
And then in my Controllers constructor receive that interface instance.
But how i can call which implementation do i need for particular condition.
Can i do this a controller ?
Or Should i use different strategy in this case.
please guide me.
Thanks.
You can have a general interface like this:
interface EmployeeStoreContract
{
public function saveAnswer();
}
Then implement this for all of the different ways you can save the answer, e.g.:
class FirstTypeEmployee implements EmployeeStoreContract
{
public function saveAnswer()
{
// do something
}
}
And in your controller to figure out how to save the request data:
public function store()
{
$employeeTypesMap = [
1 => 'FirstTypeEmployee',
2 => 'SecondTypeEmployee',
3 => 'ThirdTypeEmployee',
];
foreach ($request->all() as $answer) {
$employeeType = $answer->employeeType;
if (!array_key_exists($employeeType, $employeeTypesMap)) {
throw new \InvalidArgumentException('Answer type is not available.');
}
$employeeStrategyClass = "App\\Service\\Employee\\{$employeeTypesMap[$employeeType]}";
$employeeStrategyObject = new $employeeStrategyClass;
$employeeStrategyObject->saveAnswer($answer);
}
}
Please note the namespace where you have these classes App\Service\Employee, could be anything you want. Also feel free to make an abstract class or a trait to reuse some of the functionality of each employee class.
There can be many approaches.
I will suggest 2 ways that i would personally use:
1) Create dedicated eloquent models one for each case.
For example:
if($answer->employeeType === 1){
EmployeeOne::create($answer);
}
And your eloquent model can look like:
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use DB;
class EmployeeOne extends Model
{
protected $table = 'type_one_table';
/**
* The attributes that are mass assignable.
*
* #var array
*/
protected $fillable = [
// your db field names
];
/**
* The attributes that should be hidden for arrays.
*
* #var array
*/
protected $hidden = [
];
public $timestamps = false;
}
Make sure that the array keys of the array you pass to the create function must match your db fields. I just gave you an example of the first if so you can take it from here.
2)
Another approach would be to create a model that accepts as a parameter the table name. So based on your if you are going to pass the table name you want to insert and the data to be inserted.
if($answer->employeeType === 1){
$this->myModel->insertData($answer,'type_one_table');
}
And in your model you are going to have something like:
public function insertData($answer,$table){
$query = DB::table($table)->insertGetId($answer); // returns the id of the new record
return $query;
}
So in every if-else statement you just change the table name passing to your model function and that's it.
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();
I have created one and I thought it works:
<?php
namespace App\Traits;
use Carbon\Carbon;
trait FormatDates
{
public function setAttribute($key, $value)
{
parent::setAttribute($key, $value);
if (strtotime($value))
$this->attributes[$key] = Carbon::parse($value);
}
}
But there is a problem when calling related models. For example if you have an Article and Tag model and you want to get all tags like this:
$article->tags
it returns null because of that getter mutator.
How to fix this?
update 17.11.2017
I have found a solution to my problem. The best way to present the date in locale is to use this function:
\Carbon\Carbon::setToStringFormat("d.m.Y H:i");
simply create a service provider or a middleware and it will show all $dates in format you want. There is no need to make a getter.
Based from this: https://laravel.com/api/5.5/Illuminate/Database/Eloquent/Concerns/HasAttributes.html#method_getAttribute
The description says:
Get a plain attribute (not a relationship).
Luckily there are another two methods below it called getRelationValue and getRelationshipFromMethod, and it reads:
Get a relationship.
Get a relationship value from a method.
respectively.
And in your example, it looks like you're calling a relation.
I think you should consider it when doing your universal getter/mutator.
UPDATE:
If you inspect the code, the getAttribute also calls the getRelationValue method. But it is the last resort of the function; if the key is neither an attribute or has a mutator or is a method of the class.
Here is the stub: https://github.com/laravel/framework/blob/5.5/src/Illuminate/Database/Eloquent/Concerns/HasAttributes.php#L302
/**
* Get an attribute from the model.
*
* #param string $key
* #return mixed
*/
public function getAttribute($key)
{
if (! $key) {
return;
}
// If the attribute exists in the attribute array or has a "get" mutator we will
// get the attribute's value. Otherwise, we will proceed as if the developers
// are asking for a relationship's value. This covers both types of values.
if (array_key_exists($key, $this->attributes) ||
$this->hasGetMutator($key)) {
return $this->getAttributeValue($key);
}
// Here we will determine if the model base class itself contains this given key
// since we don't want to treat any of those methods as relationships because
// they are all intended as helper methods and none of these are relations.
if (method_exists(self::class, $key)) {
return;
}
return $this->getRelationValue($key);
}
ANOTHER UPDATE
Since you've changed your question:
You can just put the attribute name to $casts or $dates array (in your Model) so Laravel will automatically transform it into a Carbon instance when accessing it, like this:
class Article extends Model {
...
protected $dates = ['some_date_attribute`];
or with $casts
...
protected $casts = ['some_date_attributes' => 'date'];
You really can avoid this, it's already there!
on the model Class you can do:
protected $dates = ['nameOfTheDateOrTimestampTypeField','nameOfAnotherOne'];
I am using Laravel 4.2.
I have two models: User and Video, both of these models are having one-to-many relationship i.e. User -> HasMany-> Video.
Recently, I got a requirement to display the list of users along with sum of file-size of total videos uploaded by each user and allow users to be order by the sum of file size ascending or descending.
I've made following changes in User model:
class User extends Eloquent {
protected $hidden = array('videosSum');
protected $appends = array('videos_size');
public function videosSum() {
return $this->hasOne('Video')
->selectRaw('sum(file_size) as sum, user_id')
->groupBy('user_id');
}
public function getVideosSizeAttribute()
{
// if relation is not loaded already, let's do it first
if ( ! array_key_exists('videos_size', $this->relations)){
$this->load('videosSum');
}
$related = $this->getRelation('videosSum');
return $this->attributes['videos_size'] = isset($related->sum) ? (int) $related->sum : 0;
}
}
And using like:
User::where('id', '!=', Auth::user()->id);
I am getting the desired result.
But the problem is, I don't want the videos_size attribute everywhere, where the User model gets called. I want to set it dynamically.
I tried User::$appends = ['videos_size'] but it gives protected property cannot be set outsize of class error.
I also tried to make a method in User model which set the $appends if called, but it is also not working.
Can anybody help me how to enable the appends property dynamically?
Laravel doesn't support this off the bat.
my friend and I wrote this extention:
Dynamically hide certain columns when returning an Eloquent object as JSON?
basically you have to override your models.php toArray() method as appended attributes get calculated when you ask for the model in json or array form.
you can add to the trait that's in that link and use it or just put these methods in your respective model class.
public static function getStaticAppends() {
return self::$_appends;
}
public static function setStaticAppends(array $value) {
self::$_appends = $value;
return self::$_appends;
}
public static function getDefaultAppends() {
return with(new static)->getAppends();
}
public function getAppends(){
return $this->appends;
}
public function toArray() {
if (self::getStaticAppends()) {
$this->appends = self::getStaticAppends();
}
return parent::toArray();
}
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();
}
}