Modify all attributes of a Laravel model - laravel

Accessors will do their job on a single attribute perfectly, but I need a way to have a method to do an Accessor/Getter job on all attributes and automatically.
The purpose is that I want to replace some characters/numbers on getting attributes and then printing them out. I can do it from within controller and manually but I think it would be great to have it from model side and automatically.
Like overriding getAttributes() method:
public function getAttributes()
{
foreach ($this->attributes as $key => $value) {
$this->attributes[$key] = str_replace([...], [...], $value);
}
return $this->attributes;
}
But I have to call it every time on model $model->getAttributes();
Any way to do it automatically and DRY?

Try something like:
public function getAttribute($key)
{
if (array_key_exists($key, $this->attributes) || $this->hasGetMutator($key)) {
if($key === 'name') return 'modify this value';
return $this->getAttributeValue($key);
}
return $this->getRelationValue($key);
}
It's fully overriding the default method so be a bit careful.
EDIT
Also check out: http://laravel.com/docs/5.1/eloquent-mutators

I would go with following approach and override the models __get method:
public function __get($key)
{
$excluded = [
// here you should add primary or foreign keys and other values,
// that should not be touched.
// $alternatively define an $included array to whitelist values
'foreignkey',
];
// if mutator is defined for an attribute it has precedence.
if(array_key_exists($key, $this->attributes)
&& ! $this->hasGetMutator($key) && ! in_array($key, $excluded)) {
return "modified string";
}
// let everything else handle the Model class itself
return parent::__get($key);
}
}

How about running it with each Creating and Updating events. So you can do something like that:
public function boot()
{
Model::creating(function ($model)
return $model->getAttributes(); //or $this->getAttributes()
});
Model::updating(function ($model)
return $model->getAttributes(); //or $this->getAttributes()
});
}

Related

Retrieve parameters from custom validation object

I have basic custom validation rule. In
public function passes($attribute, $value)
{
foreach ($parameters as $key)
{
if ( ! empty(Input::get($key)) )
{
return false;
}
}
return true;
}
I have my rule defined. I, although need to retrieve parameters from the rule but the passes method does not provide it as an argument.
If I would use the style Validator:extends... that provides 4 arguments: $attribute, $value, $parameters, $validator. Then I could use the parameters easily, unfortunatelly I have to use this way.
EDIT:
To clear the question. I want to retrieve the parameters of the rule, like so in other way of coding it:
'not_empty:user_id'. The array of values behind the colon.
Edit:---
The custom rule object is simply an object. If you want to pass it any more parameters you can in it's constructor:
$request->validate([
'name' => ['required', new MyCustomRule('param', true, $foo)],
]);
Then save those and use them in your passes function.
public function __construct($myCustomParam){
$this->myCustomParam = $myCustomParam;
}
Then in your passes function use:
$this->myCustomParam
I believe the only way is to retrieve it from the request when using rule objects.
For example:
public function passes($attribute, $value)
{
foreach ($parameters as $key) {
// Or using \Request::input($key) if you want to use the facade
if (!empty(request()->input($key)) {
return false;
}
}
return true;
}

Laravel dynamic scope only works first time

I'm missing something with how the global scopes work in Laravel 5.5.
In my controller, index , I am passing filters into a getter:
public function index(SaleFilters $filters)
{
return new SaleCollection($this->getSales($filters));
}
getSales:
protected function getSales(SaleFilters $filters)
{
$sales = Sale::with('office')->filter($filters);
return $sales->paginate(50);
}
protected function range($range)
{
$dates = explode(" ", $range);
if (count($dates) == 2) {
$this->builder = Sale::with(['office', 'staff'])
->where('sale_date', '>=', $dates[0])
->where('sale_date', '<', $dates[1])
->orderBy('sale_date', 'desc');
return $this->builder;
}
return false;
}
I have a scope setup in the sale model as such, which I would have thought would apply to the above filter automatically ? If not, do I have to reapply the same scope, duplicating the scope code in the filter ?
protected static function boot()
{
parent::boot();
$user = Auth::user();
if (($user) && ($user['office_id'])) {
return Sale::ofOffice($user['office_id'])->get();
}
}
public function scopeOfOffice($query, $office)
{
return $query->where('office_id', $office);
}
So basically, IF the user has an office_id applied, it should apply the ofOffice scope, therefore it should only ever return the sales that apply to that office_id.
Basically it works on page load via axios GET request
Route::get('/sales', 'SalesController#index')->middleware('auth:api');
axios
.get('api/sales/?range=" + this.rangeFilter)
rangeFilter is basically a start and end date passed into the above filter query.
Can anyone shed some light on how the scopes really work or if anything is obvious as to why its not always working? As I said, it works on page load where I provide default values for the rangeFilter, however when I change those days and it refetches via the same axios call, it seems to not be applying the scope, and I get ALL results instead of where office_id = 'x'
As far as i'm concerned, the range filter above would be executing on the first page load as well, so not sure why it would apply there, and not afterwards.
You should not mix the use of dynamic scope with global one. Also, static boot function does not expect a return. In order to use dynamic scope, you need to call it every time you need it. Hence, the name is dynamic. Query applied is not always executed by default. There so,
protected function getSales(SaleFilters $filters)
{
$sales = Sale::ofOffice($anyOfficeHere)->with('office')->filter($filters);
return $sales->paginate(50);
}
To suit your existing code, you may want to add an if statement in your model. Then call the scope function without argument.
public function scopeOfOffice($q)
{
if (($user = \Auth::user()) && ($office = $user->office_id)) {
$q->where('office_id', $office);
}
}
// Your controller
protected function getSales(SaleFilters $filters)
{
$sales = Sale::ofOffice()->with('office')->filter($filters);
return $sales->paginate(50);
}
If you feel so much cumbersome to type ofOffice repeatedly. A global scope is the way to go. Within your model static boot function, you can also apply anonymous function if you feel creating a separated class kinda bloat your apps.
protected static function boot()
{
parent::boot();
static::addGlobalScope('officeOrWhatNot', function ($q) {
if (($user = \Auth::user()) && ($office = $user->office_id)) {
$q->where('office_id', $office);
}
});
}
// Your controller. No more `ofOffice`, it's automatically applied.
protected function getSales(SaleFilters $filters)
{
$sales = Sale::with('office')->filter($filters);
return $sales->paginate(50);
}

Adding custom eloquent attribute in Laravel 5

I have a function named siblings which fetches all siblings of a user.
select siblings(id) as `siblings` from users where id = 1
I can access the function in Eloquent as
User::where('id', 1)->first([DB::raw(siblings(id) as `siblings`)]->siblings;
I want to make the siblings available via custom attribute.
I added siblings to $appends array
I also created getSiblingsAttribute method in my User model as
public function getSiblingsAttribute()
{
if (!$this->exists()) {
return [];
}
$siblings = User::where('idd', $this->id)
->first([DB::raw('siblings(id) AS `siblings`')])
->siblings;
return explode(',', $siblings);
}
But this is not working as $this->id returns null
My table schema is users(id, username,...), so clearly id is present.
Is there a way by which I can bind the siblings function while querying db and then returning something like $this->siblings from getSiblingsAttribute. If I can bind siblings(id) as siblings with query select globally as we do for scopes using global scope.
That way my code can be simply
public function getSiblingsAttribute()
{
return $this->siblings;
}
The simplest way is to create a view in your database and use that as a table:
protected $table = 'user_view';
Otherwise I need more information about your id == null problem.
If you can fix this by your own in the next step it is important that you use an other column name by selecting as in your accessor otherwise you run in an infinite loop.
public function getSiblingsAttribute()
{
if (!$this->exists()) {
return [];
}
$siblings = User::where('id', $this->id)
->first([DB::raw('siblings(id) AS `siblings_value`')])
->siblings_value;
return explode(',', $siblings);
}
EDIT
Sadly there is no simple way to archieve this.
But after a little bit tinkering I have found a (not very nice) solution.
Give it a try.
You have to add the following class and trait to your app.
app/Classes/AdditionalColumnsTrait.php (additional column trait)
namespace App\Classes;
trait AdditionalColumnsTrait {
public function newEloquentBuilder($query) {
$builder = new EloquentBuilder($query);
$builder->additionalColumns = $this->getAdditionalColumns();
return $builder;
}
protected function getAdditionalColumns() {
return [];
}
}
app/Classes/EloquentBuilder.php (extended EloquentBuilder)
namespace App\Classes;
use Illuminate\Database\Eloquent\Builder;
class EloquentBuilder extends Builder {
public $additionalColumns = [];
public function getModels($columns = ['*']) {
$oldColumns = is_null($this->query->columns) ? [] : $this->query->columns;
$withTablePrefix = $this->getModel()->getTable() . '.*';
if (in_array('*', $columns) && !in_array($withTablePrefix, $oldColumns)) {
$this->query->addSelect(array_merge($columns, array_values($this->additionalColumns)));
} elseif (in_array($withTablePrefix, $oldColumns)) {
$this->query->addSelect(array_values($this->additionalColumns));
} else {
foreach ($this->additionalColumns as $name => $additionalColumn) {
if (!is_string($name)) {
$name = $additionalColumn;
}
if (in_array($name, $columns)) {
if (($key = array_search($name, $columns)) !== false) {
unset($columns[$key]);
}
$this->query->addSelect($additionalColumn);
}
}
if (is_null($oldColumns)) {
$this->query->addSelect($columns);
}
}
return parent::getModels($columns);
}
}
after that you can edit your model like this:
class User extends Model {
...
use App\Classes\AdditionalColumnsTrait;
protected function getAdditionalColumns() {
return [
'siblings' => DB::raw(siblings(id) as siblings)),
];
}
...
}
now your siblings column will be selected by default.
Also you have the option to select only specific columns.
If you don't want to select the additional columns you can use: User::find(['users.*']).
Perhaps it is a solution for you.

Routing to controller with optional parameters

I'd like to create a route that takes a required ID, and optional start and end dates ('Ymd'). If dates are omitted, they fall back to a default. (Say last 30 days) and call a controller....lets say 'path#index'
Route::get('/path/{id}/{start?}/{end?}', function($id, $start=null, $end=null)
{
if(!$start)
{
//set start
}
if(!$end)
{
//set end
}
// What is the syntax that goes here to call 'path#index' with $id, $start, and $end?
});
There is no way to call a controller from a Route:::get closure.
Use:
Route::get('/path/{id}/{start?}/{end?}', 'Controller#index');
and handle the parameters in the controller function:
public function index($id, $start = null, $end = null)
{
if (!$start) {
// set start
}
if (!$end) {
// set end
}
// do other stuff
}
This helped me simplify the optional routes parameters (From Laravel Docs):
Occasionally you may need to specify a route parameter, but make the presence of that route parameter optional. You may do so by placing a ? mark after the parameter name. Make sure to give the route's corresponding variable a default value:
Route::get('user/{name?}', function ($name = null) {
return $name;
});
Route::get('user/{name?}', function ($name = 'John') {
return $name;
});
Or if you have a controller call action in your routes then you could do this:
web.php
Route::get('user/{name?}', 'UsersController#index')->name('user.index');
userscontroller.php
public function index($name = 'John') {
// Do something here
}
I hope this helps someone simplify the optional parameters as it did me!
Laravel 5.6 Routing Parameters - Optional parameters
I would handle it with three paths:
Route::get('/path/{id}/{start}/{end}, ...);
Route::get('/path/{id}/{start}, ...);
Route::get('/path/{id}, ...);
Note the order - you want the full path checked first.
Route::get('user/{name?}', function ($name = null) {
return $name;
});
Find more details here (Laravel 7) : https://laravel.com/docs/7.x/routing#parameters-optional-parameters
You can call a controller action from a route closure like this:
Route::get('{slug}', function ($slug, Request $request) {
$app = app();
$locale = $app->getLocale();
// search for an offer with the given slug
$offer = \App\Offer::whereTranslation('slug', $slug, $locale)->first();
if($offer) {
$controller = $app->make(\App\Http\Controllers\OfferController::class);
return $controller->callAction('show', [$offer, $campaign = NULL]);
} else {
// if no offer is found, search for a campaign with the given slug
$campaign = \App\Campaign::whereTranslation('slug', $slug, $locale)->first();
if($campaign) {
$controller = $app->make(\App\Http\Controllers\CampaignController::class);
return $controller->callAction('show', [$campaign]);
}
}
throw new \Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
});
What I did was set the optional parameters as query parameters like so:
Example URL:
/getStuff/2019-08-27?type=0&color=red
Route:
Route::get('/getStuff/{date}','Stuff\StuffController#getStuff');
Controller:
public function getStuff($date)
{
// Optional parameters
$type = Input::get("type");
$color = Input::get("color");
}
Solution to your problem without much changes
Route::get('/path/{id}/{start?}/{end?}', function($id, $start=null, $end=null)
{
if(empty($start))
{
$start = Carbon::now()->subDays(30)->format('Y-m-d');
}
if(empty($end))
{
$end = Carbon::now()->subDays(30)->format('Y-m-d');
}
return App\Http\Controllers\HomeController::Path($id,$start,$end);
});
and then
class HomeController extends Controller
{
public static function Path($id, $start, $end)
{
return view('view');
}
}
now the optimal approach is
use App\Http\Controllers\HomeController;
Route::get('/path/{id}/{start?}/{end?}', [HomeController::class, 'Path']);
then
class HomeController extends Controller
{
public function Path(Request $request)
{
if(empty($start))
{
$start = Carbon::now()->subDays(30)->format('Y-m-d');
}
if(empty($end))
{
$end = Carbon::now()->subDays(30)->format('Y-m-d');
}
//your code
return view('view');
}
}

Route resources set before auth not working in Laravel 4

I added this in routes.php, expected it will check the authentication session for the page, however it is not working.
Route::resource('ticket', 'TicketController', array('before' => 'auth') );
Then I go to the controller, work in another way. It's work.
class TicketController extends BaseController {
public function __construct()
{
$this->beforeFilter('auth');
}
May I know where can get more documentation regarding the Route::resource(), what type of argument it able to accept?
OK... I found the answer.
in
\vendor\laravel\framework\src\Illuminate\Routing\Router.php
public function resource($resource, $controller, array $options = array())
{
// If the resource name contains a slash, we will assume the developer wishes to
// register these resource routes with a prefix so we will set that up out of
// the box so they don't have to mess with it. Otherwise, we will continue.
if (str_contains($resource, '/'))
{
$this->prefixedResource($resource, $controller, $options);
return;
}
// We need to extract the base resource from the resource name. Nested resources
// are supported in the framework, but we need to know what name to use for a
// place-holder on the route wildcards, which should be the base resources.
$base = $this->getBaseResource($resource);
$defaults = $this->resourceDefaults;
foreach ($this->getResourceMethods($defaults, $options) as $method)
{
$this->{'addResource'.ucfirst($method)}($resource, $base, $controller);
}
}
protected function getResourceMethods($defaults, $options)
{
if (isset($options['only']))
{
return array_intersect($defaults, $options['only']);
}
elseif (isset($options['except']))
{
return array_diff($defaults, $options['except']);
}
return $defaults;
}
as you can see, it only accept only and except arguement only.
If you want to archive the same result in route.php, it can be done as below
Route::group(array('before'=>'auth'), function() {
Route::resource('ticket', 'TicketController');
});

Resources