Is it possible to temporarily disable the appends functionality in Laravel 5.4 during testing?
protected $appends = [
'full_name',
];
I want to ignore that ^.
I've made a model factory but when I'm testing I don't want to have these append items on my model.
I have had experience with this too. I've found a good solution here.
But, if you like a one-liner solution, you can also use the ff methods of Eloquent's Model class:
setHidden(array $hidden)
makeHidden(array|string $attributes)
You can check these here.
I was using this code is suitable:
testing for Model name Product for example
// get product with "id = 1" for example
$needed_product = Product::find(1)->toArray();
// remove un-used attributes
$product = new Product;
foreach ($product->appends as $attr) {
unset($needed_product[$attr]);
}
Now the $needed_product gets without any appends attributes
I was thinking something like this:
/**
* Get all appended items.
*
* #return array
*/
public function getAppends()
{
$vars = get_class_vars(__CLASS__);
return $vars['appends'];
}
/**
* Unset all appended items.
*
* #return $this
*/
public function unsetAppends()
{
collect($this->getAttributes())->pull($this->getAppends());
return $this;
}
But #elegisandi thanks that works great.
Related
I've tried to combine the Laravel docs on implicit binding and optional parameters and have the following code.
routes file:
Route::get('go/{example?}', [ExampleController::class, 'click'])->name('example');
And in the controller:
public function click(Example $example = null)
{
// Execution never reaches here
}
Execution never reaches the controller unless there is an Example with the correct slug, as it throws a 404. I want to check if $example is null in the controller and use custom logic there. How can this be accomplished?
Try this
Route::get('go/{example?}', [ExampleController::class, 'click'])->name('example');
public function click($example)
{
if($example != null){
$example = Example::findOrfail($example);
}
}
in model binding it will automatically run findOrfail to that model so don't you that so you will have control over it then you can manage
the #ettdro answer is perfect (and all credit is to him), but i think an answer with actual code would be useful:
routes:
Route::get('go/{example?}', [ExampleController::class, 'click'])->name('example');
controller:
public function click(Example $example)
{
// Stuff
}
Model of Example:
/**
* Retrieve the model for a bound value.
*
* #param mixed $value
* #param string|null $field
* #return \Illuminate\Database\Eloquent\Model|null
*/
public function resolveRouteBinding($value, $field = null)
{
$result=$this->where('id', $value)->first();
return ($result)?$result:new Example();
}
You should obtain in the controller always a valid object, empty or not.
Had the same problem, and i'm happy with this solution.
To do that, you need use 'id' as your primary key in database and model,
if you are using another name for your pimary key, then you need to define it at your route:
Route::get('go/{example:number?}', [...]);
So for my project model setAppends([]) works as below:
Project::find($projectId)->setAppends([])
but what if I want to set appends to empty array for a relation which I'm eager loading with with, like below:
$project = Project::with('pages')->find($projectId);
->setAppends([]) not working in above code, as it will set it to empty array for Project not for Page.
Can anyone guide how to achieve that ?
Update:
page.php Model has appends and hidden like this:
class Page extends Model {
// I don't want to load this (`appends`) attributes when I call Project::find($projectId)
protected $appends = ['thumbnail_url', 'total_annotations', 'total_tasks', 'total_done_tasks', 'image_url', 'edited_data_items_count'];
protected $hidden = ['tasksCount', 'doneTasksCount', 'annotationsCount', 'xsl', 'xml', 'dataxml_version', 'sort_order', 'editedDataItemsCount', 'deletedDataItemsCount'];
}
Project.php model looks like this:
class Project extends Model {
use SoftDeletes;
protected $appends = ['total_tasks', 'total_done_tasks', 'total_pages', 'total_annotations', 'edited_dataitems_total_count'];
protected $hidden = ['tasksCount', 'doneTasksCount', 'pagesCount', 'annotationsCount', 'folder_path', 'attachment_url', 'pages'];
}
On Project you may provide a static method, which allows you to iterate over the eagerly loaded pages and adjust their append-array.
class Project
{
...
public static function eagerFindWithoutAppends($projectId)
{
$model = self::with('pages')->find($projectId);
$model->setAppends([]);
foreach ($model->pages as $page) {
$page->setAppends([]);
}
return $model;
}
...
}
But if I understand correctly, the dynamic data in your Pages class does more than just providing convenient shortcuts based on the regularly loaded data (such as something like getFullName which would combine first_name and last_name).
What do your appends do?
I don't want to load this (appends) attributes
Another possible solution I could think of is to inherit NoneAppendPages from Pages and override $append and all the related get... methods.
Then in Project declare another relationship to NoneAppendPages next to Pages. You then eager load Project::::with('none_append_pages')->find($projectId);
class NoneAppendPages extends Pages
{
protected $appends = [];
getYourDynamicAttributeMethodName() { return null; } // for all your appends
}
class Project
{
public function pages()
{
// I don't know what relationship you declared / assuming on to many
return $this->hasMany('App\Page');
}
public function noneAppendPages()
{
// declare the same way you did with pages
return $this->hasMany('App\NoneAppendPage');
}
}
The given solution does not work when using a package that does a lot of the work after you define the with() relations like datatables
here is a solution that works for any model.
<?php
namespace App\Database;
trait Appendable {
static protected $static_appends = [];
static protected $static_replace_appends = null;
/**
* set a static appends array to add to or replace the existing appends array..
* replace => totally replaces the existing models appends array at time of calling getArrayableAppends
* add => merges and then makes unique. when getArrayableAppends is called. also merges with the existing static_appends array
*
* #param $appendsArray
* #param bool $replaceExisting
*/
public static function setStaticAppends($appendsArray, $replaceExisting = true)
{
if($replaceExisting) {
static::$static_replace_appends = true;
static::$static_appends = array_unique($appendsArray);
} else {
static::$static_replace_appends = false;
static::$static_appends = array_unique(array_merge(static::$static_appends,$appendsArray));
}
}
/**
* Get all of the appendable values that are arrayable.
*
* #return array
*/
protected function getArrayableAppends()
{
if(!is_null(static::$static_replace_appends)) {
if(static::$static_replace_appends) {
$this->appends = array_unique(array_merge(static::$static_appends,$this->appends??[]));
} else {
$this->appends = static::$static_appends;
}
}
return parent::getArrayableAppends();
}
}
then you can just apply the trait to any model
<?php
namespace App\Database;
abstract class Company
{
use Appendable;
}
then call the static method BEFORE you use the relationship
<?php
$replaceCurrentAppendsArray = true;
// this will remove the original appends by replacing with empty array
\App\Database\Company::setStaticAppends([],$replaceCurrentAppendsArray);
$replaceCurrentAppendsArray = true;
// this will remove the original appends by replacing with smaller array
\App\Database\Company::setStaticAppends(['thumbnail_url'],$replaceCurrentAppendsArray);
$replaceCurrentAppendsArray = FALSE;
// this will add to the original appends by providing an additional array element
\App\Database\Company::setStaticAppends(['my_other_attribute'],$replaceCurrentAppendsArray);
this will allow you to override the appends array provided on the model even if another package is going to be loading the model. Like yajra/laravel-datatable where my issue was and brought me to this page which inspired a more dynamic solution.
This is similar to Stefan's second approach, but this is more dynamic so you do not have to create additional model extensions to accomplish the overrides.
You could take a similar approach to override the HidesAttribute trait as well.
What I'm trying to do is to append the comments of each article to the articles object, but the problem is that I need to request different number of comments each time.
and for some reason I need to use mutators for that, because some times I request 50 articles and I don't want to loop through the result and append the comments.
So is it possible to do something like the following and how to pass the extra argument.
This the Model:
class Article extends Model
{
protected $appends = ['user', 'comments', 'media'];
public function getCommentsAttribute($data, $maxNumberOfComments = 0)
{
// I need to set maxNumberOfComments
return $this->comments()->paginate($maxNumberOfComments);
}
}
Here is the controller:
class PostsController extends Controller
{
public function index()
{
//This will automatically append the comments to each article but I
//have no control over the number of comments
$posts = Post::user()->paginate(10);
return $posts;
}
}
What I don't want to do is:
class PostsController extends Controller
{
public function index()
{
$articles = Post::user()->all();
$number = 5;
User::find(1)->articles()->map(function(Article $article) {
$article['comments'] = $article->getCommnets($number);
return $article;
});
return Response::json($articles);
}
}
Is there a better way to do it? because I use this a lot and it does not seams right.
Judging from the Laravel source code, no – it's not possible to pass an extra argument to this magic accessor method.
The easiest solution is just to add another, extra method in your class that does accept any parameters you wish – and you can use that method instead of magic property.
Eg. simply rename your getCommentsAttribute() to getComments() and fire ->getComments() instead of ->comments in your view, and you are good to go.
I just set a public property on the model. At the accessing point, I update that property to my desired value. Then, in the attribute method, I read the desired arguments from that property. So, putting all of that together,
// Model.php
public $arg1= true;
public function getAmazingAttribute () {
if ($this->arg1 === false)
$this->relation()->where('col', 5);
else $this->relation()->where('col', 15);
}
// ModelController.php
$instance->arg1 = false;
$instance->append('amazing');
It is been a while for this question, but maybe someone will need it too.
Here is my way
{
/**
* #var string|null
*/
protected ?string $filter = null;
/**
* #return UserSettings[]|null
*/
public function getSettingsAttribute(): ?array
{
return services()->tenants()->settings($this)->getAll();
}
/**
* #return FeatureProperty[]|null
*/
public function getFeaturePropertiesAttribute(): ?array
{
return services()->tenants()->featureProperty($this)->getListByIds($this->filter);
}
/**
* #param string|null $filter
* #return Tenant
*/
public function filter(string $filter = null): Model
{
$this->filter = $filter;
return $this;
}
Accessor is using some service to get values. Service accepts parameters, in my case string, that will be compared with featureProperty->name
Magic happens when you return $this in filter method.
Regular way to call accessor would be:
$model->feature_properties
Extended way:
$model->filter('name')->feature_properties
Since filter argument can be null, we can have accessor like this:
$filter = null
$model->filter($filter)->feature_properties
In case you would like to play with it a little more you can think about overriding models getAttribute or magic __call methods implementing filter in manner which will be similar to laravel scopes
I know its an old question, but there is another option, but maybe not the best:
$articles = Post::user()->all();
$number = 5;
$articles->map(function($a) use($number){
$a->commentsLimit = $number;
return $a;
});
And then in getCommentsAttribute():
return $this->comments()->paginate($this->commentsLimit);
While using Laravel 5.1, I am trying to check every value before it is saved in the database using Eloquent ORM. My logic is, first trim the value, if the value is an empty string "", then to convert it to null instead of just an empty string.
I was advised to create a Trait which will override the setAttribute method for that.
So here is what I have done
I have a new folder "app\Traits" inside of a file called TrimScalarValues.php which contains the following code
<?php
namespace App\Traits;
trait TrimScalarValues
{
public function setAttribute($key, $value)
{
if (is_scalar($value)) {
$value = $this->emptyStringToNull(trim($value));
}
return $this->setAttribute($key, $value);
}
/**
* return null value if the string is empty otherwise it returns what every the value is
*
*/
private function emptyStringToNull($string)
{
//trim every value
$string = trim($string);
if ($string === ''){
return null;
}
return $string;
}
}
Finally I have a app\Models\Account.php file which contains the following code
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use App\Models\industry;
use App\Traits\RecordSignature;
use App\Traits\TrimScalarValues;
class Account extends Model
{
use RecordSignature, TrimScalarValues;
/**
* The database table used by the model.
*
* #var string
*/
protected $table = 'accounts';
protected $primaryKey = 'account_id';
const CREATED_AT = 'created_on';
const UPDATED_AT = 'modified_on';
const REMOVED_AT = 'purged_on';
/**
* The attributes that are mass assignable.
*
* #var array
*/
protected $fillable = ['client_id','account_name', 'company_code', 'legal_name', 'created_by','modified_by','instrucations'];
/**
* The attributes excluded from the model's JSON form.
*
* #var array
*/
//protected $hidden = ['account_id', 'remember_token'];
protected $guarded = ['account_id'];
/**
* Get the industry record associated with the account.
*/
public function industry()
{
return $this->hasOne(industry, industry::primaryKey);
}
public function pk(){
return $this->primaryKey;
}
}
But every time I update a value, I get a white page with no error or logs.
When I modify the app\Models\Account.php and change use RecordSignature, TrimScalarValues; to use RecordSignature; then I do not get a white page but obviously the values are not trimmed and converted to null.
What am I doing wrong here?
You can't call $this->setAttribute() in your trait. Instead you want to call the "original" setAttribute method by using parent:::
public function setAttribute($key, $value)
{
if (is_scalar($value)) {
$value = $this->emptyStringToNull(trim($value));
}
return parent::setAttribute($key, $value);
}
Regarding the empty logs, have you checked the webserver log besides the one from the framework?
I had the same problem and solved it by creating a middleware that filters empty input fields.
public function handle($request, Closure $next) {
$input = $request->all();
if ($input) {
array_walk_recursive($input, function (&$item) {
$item = trim($item);
$item = ($item == "") ? null : $item;
});
$request->merge($input);
}
return $next($request);
}
Don't forget to add your custom middleware to Http/Kernel.php
Found this at Laracasts
You might wanna take a look at this package:
https://packagist.org/packages/iatstuti/laravel-nullable-fields
"This create a trait and allows you to easily flag attributes that should be set as null when being persisted to the database."
I know this post is old, and at that time this package maybe didn't exist yet.
You can use mutator in you model.
For the field account_name mutator should looks like this:
public function setAccountNameAttribute($account_name)
{
if(is_null($account_name))
{
$this->attributes['account_name'] = null;
}
else
{
$this->attributes['account_name'] = $account_name;
}
}
And everytime when you will update or insert the record using Eloquent, account_name will be passed through this mutator.
I am having trouble with global scopes, especially the removal of the scope.
In my User model, i have a ActivatedUsersTrait, that introduces a global scope to only query for Users with the column "activated" set to true (The User is "activated" after email verification).
So far everything works fine, when i query for User::all(), i only get Users with activated=true.
My problem now is, how to include the non-activated Users into my query, like SoftDeletingTrait does via withTrashed()? This is only relevant in my ActivationController, where i need to get the User, set activated=true and save them back to db.
I've created a withInactive() method in my ActiveUsersTrait, based on the method i found in SoftDeletingTrait, but when i run a query on User::withInactive->get(), the non-activated Users won't show up in the results.
Here's my ActiveUsersTrait:
use PB\Scopes\ActiveUsersScope;
trait ActiveUsersTrait {
public static function bootActiveUsersTrait()
{
static::addGlobalScope(new ActiveUsersScope);
}
public static function withInactive()
{
// dd(new static);
return (new static)->newQueryWithoutScope(new ActiveUsersScope);
}
public function getActivatedColumn()
{
return 'activated';
}
public function getQualifiedActivatedColumn()
{
return $this->getTable().'.'.$this->getActivatedColumn();
}
}
and my ActiveUsersScope:
use Illuminate\Database\Eloquent\ScopeInterface;
use Illuminate\Database\Eloquent\Builder;
class ActiveUsersScope implements ScopeInterface {
public function apply(Builder $builder)
{
$model = $builder->getModel();
$builder->where($model->getQualifiedActivatedColumn(), true);
}
public function remove(Builder $builder)
{
$column = $builder->getModel()->getQualifiedActivatedColumn();
$query = $builder->getQuery();
foreach ((array) $query->wheres as $key => $where)
{
if ($this->isActiveUsersConstraint($where, $column))
{
unset($query->wheres[$key]);
$query->wheres = array_values($query->wheres);
}
}
}
protected function isActiveUsersConstraint(array $where, $column)
{
return $where['type'] == 'Basic' && $where['column'] == $column;
}
}
Any help is highly appreciated!
Thanks in advance! -Joseph
Eloquent queries now have a removeGlobalScopes() method.
See: https://laravel.com/docs/5.3/eloquent#query-scopes (under the Removing Global Scopes subheading).
From the docs:
// Remove one scope
User::withoutGlobalScope(AgeScope::class)->get();
// Remove all of the global scopes...
User::withoutGlobalScopes()->get();
// Remove some of the global scopes...
User::withoutGlobalScopes([
FirstScope::class, SecondScope::class
])->get();
The SoftDeletingTrait where cleanup is simpler because it doesn't involve any bindings (it's a "Null" where, not a "Basic" where). The issue you're encountering is that the binding for [ n => true ] is still there, even when you manually remove the where.
I'm thinking about making a PR because I encountered the same issue myself, and there isn't a great way to keep track of which wheres and which bindings go together.
If you are only using a simple query, you can keep track of the index of the binding more or less like so:
use Illuminate\Database\Eloquent\ScopeInterface;
use Illuminate\Database\Eloquent\Builder;
class ActiveUsersScope implements ScopeInterface {
/**
* The index in which we added a where clause
* #var int
*/
private $where_index;
/**
* The index in which we added a where binding
* #var int
*/
private $binding_index;
/**
* Apply the scope to a given Eloquent query builder.
*
* #param \Illuminate\Database\Eloquent\Builder $builder
* #return void
*/
public function apply(Builder $builder)
{
$model = $builder->getModel();
$builder->where($model->getQualifiedActivatedColumn(), true);
$this->where_index = count($query->wheres) - 1;
$this->binding_index = count($query->getRawBindings()['where']) - 1;
}
/**
* Remove the scope from the given Eloquent query builder.
*
* #param \Illuminate\Database\Eloquent\Builder $builder
* #return void
*/
public function remove(Builder $builder)
{
$query = $builder->getQuery();
unset($query->wheres[$this->where_index]);
$where_bindings = $query->getRawBindings()['where'];
unset($where_bindings[$this->binding_index]);
$query->setBindings(array_values($where_bindings));
$query->wheres = array_values($query->wheres);
}
}
Note how we're storing the indices where the where clause and bindings were added, rather than looping through and checking if we found the right one. This almost feels like a better design—we added the where clause and binding, so we should know where it is without having to loop through all where clauses. Of course, it will all go haywire if something else (like ::withTrashed) is also messing with the where array. Unfortunately, the where bindings and where clauses are just flat arrays, so we can't exactly listen on changes to them. A more object-oriented approach with better automatic management of the dependency between clauses and their binding(s) would be preferred.
Obviously this approach could benefit from some prettier code and validation that array keys exists, etc. But this should get you started. Since the global scopes aren't singletons (they get applied whenever newQuery() is invoked) this approach should be valid without that extra validation.
Hope this helps under the heading of "good enough for now"!
Just found this after having the same problem and I have a more eloquent solution. Simply replace your "remove" method with this.
/**
* Remove the scope from the given Eloquent query builder.
*
* #param \Illuminate\Database\Eloquent\Builder $builder
* #return void
*/
public function remove(Builder $builder)
{
$query = $builder->getQuery();
$column = $builder->getModel()->getQualifiedActivatedColumn();
foreach ((array) $query->wheres as $key => $where)
{
if ($this->isActiveUsersConstraint($where, $column))
{
// Here SoftDeletingScope simply removes the where
// but since we use Basic where (not Null type)
// we need to get rid of the binding as well
$this->removeWhere($query, $key);
$this->removeBinding($query, $key);
}
}
}
/**
* Remove scope constraint from the query.
*
* #param \Illuminate\Database\Eloquent\Builder $builder
* #param int $key
* #return void
*/
protected function removeWhere($query, $key)
{
unset($query->wheres[$key]);
$query->wheres = array_values($query->wheres);
}
/**
* Remove scope constraint from the query.
*
* #param \Illuminate\Database\Eloquent\Builder $builder
* #param int $key
* #return void
*/
protected function removeBinding($query, $key)
{
$bindings = $query->getRawBindings()['where'];
unset($bindings[$key]);
$query->setBindings(array_values($bindings));
}
Look at the newQuey function (Eloquent/Model.php Lvl 4.2):
newQuery{
//'initialized'
$b = $this->newQueryWithoutScopes(); //Get a 'clean' query. Note the plural.
//and applies scopes
return $this->applyGlobalScopes($b); //now builder is 'dirty'
}
So, this suggests a solution:
function someButNotAllScopes(){
$b = $this->newQueryWithoutScopes();
$unwanted = new MyUnwantedScope();
//get all scopes, but skip the one(s) you dont want
foreach($this->getGlobalScopes as $s){
if ($s instanceof $unwanted){continue;}
$s->apply($b, $this)
}
return $b;
}
You can also do something clever with scopes. Make em implement and OnOffInterface with an 'applyMe' method. This method can 'turn' on/off the apply method of a scope. In the function above you can get the unswanted scope and 'turn' if off:
$scope = $this->getGlobalScope(new Unwanted());
$scope->applyme(false); //will turn off the apply method
return $this->newQuery(); //now each scope checks if it is 'off'
Simply, you can apply without bindings like so:
$builder->where($column, new \Illuminate\Database\Query\Expression(1));
or
$builder->where($column, \DB::raw(1));