Yii2. Adding attribute and rule dynamically to model - php

I am writing a widget and I want to avoid user adding code to their model (I know it would be easier but using it to learn something new).
Do you know if it is possible to add an attribute (which is not in your database, so it will be virtual) to a model and add a rule for that attribute?. You have no access to change that model code.
I know rules is an array. In the past I have merged rules from parent class using array_merge. Can it be done externally? Does Yii2 has a method for that?
An idea is to extend model provided by the user with a "model" inside my widget an there use:
public function init() {
/*Since it is extended this not even would be necessary,
I could declare the attribute as usual*/
$attribute = "categories";
$this->{$attribute} = null; //To create attribute on the fly
parent::init();
}
public function rules() {
$rules = [...];
//Then here merge parent rules with mine.
return array_merge(parent::rules, $rules);
}
But If I extend it, when I use that model in an ActiveForm in example for a checkbox, it will use my "CustomModel", so I want to avoid that. Any other ideas? How to do it without extending their model?

Add Dynamic Attributes to a existing Model
When you want to add dynamic attributes during runtime to a existing model. Then you need some custom code, you need: A Model-Class, and a extended class, which will do the dynamic part and which have array to hold the dynamic information. These array will merged in the needed function with the return arrays of the Model-Class.
Here is a kind of mockup, it's not fully working. But maybe you get an idea what you need to do:
class MyDynamicModel extends MyNoneDynamicModel
{
private $dynamicFields = [];
private $dynamicRules = [];
public function setDynamicFields($aryDynamics) {
$this->dynamicFields = $aryDynamics;
}
public function setDynamicRules($aryDynamics) {
$this->dynamicRules = $aryDynamics;
}
public function __get($name)
{
if (isset($this->dynamicFields[$name])) {
return $this->dynamicFields[$name];
}
return parent::__get($name);
}
public function __set($name, $value)
{
if (isset($this->dynamicFields[$name])) {
return $this->dynamicFields[$name] = $value;
}
return parent::__set($name, $value);
}
public function rules() {
return array_merge(parent::rules, $this->dynamicRules);
}
}
Full Dynamic Attributes
When all attributes are dynamic and you don't need a database. Then use the new DynamicModel of Yii2. The doc states also:
DynamicModel is a model class primarily used to support ad hoc data validation.
Here is a full example with form integration from the Yii2-Wiki, so i don't make a example here.
Virtual Attributes
When you want to add a attribute to the model, which is not in the database. Then just declare a public variable in the model:
public $myVirtualAttribute;
Then you can just use it in the rules like the other (database-)attributes.
To do Massive Assignment don't forget to add a safe rule to the model rules:
public function rules()
{
return [
...,
[['myVirtualAttribute'], 'safe'],
...
];
}
The reason for this is very well explained here:
Yii2 non-DB (or virtual) attribute isn't populated during massive assignment?

Related

Create dynamic Laravel accessor

I have a Product model and also an Attribute model. The relationship between Product and Attribute is many to many. On my Product model I am trying to create a dynamic accessor. I am familiar with Laravel's accessors and mutators feature as documented here. The problem I am having is that I do not want to create an accessor every time I create a product attribute.
For example, A product may have a color attribute which could be setup like so:
/**
* Get the product's color.
*
* #param string $value
* #return string
*/
public function getColorAttribute($value)
{
foreach ($this->productAttributes as $attribute) {
if ($attribute->code === 'color') {
return $attribute->pivot->value;
}
}
return null;
}
The product's color could then be accessed like so $product->color.
If I where to add a size attribute to the product I would need to setup another accessor on the Product model so I could access that like so $product->size.
Is there a way I can setup a single "dynamic" accessor to handle all of my attributes when accessed as a property?
Do I need to override Laravel's accessor functionality with my own?
Yes, you can add your own piece of logic into the getAttribute() function of the Eloquent Model class (override it in your model), but in my opinion, it's not a good practice.
Maybe you can have a function:
public function getProductAttr($name)
{
foreach ($this->productAttributes as $attribute) {
if ($attribute->code === $name) {
return $attribute->pivot->value;
}
}
return null;
}
And call it like this:
$model->getProductAttr('color');
Override Magic method - __get() method.
Try this.
public function __get($key)
{
foreach ($this->productAttributes as $attribute) {
if ($attribute->code === $key) {
return $attribute->pivot->value;
}
}
return parent::__get($key);
}
I think probably Олег Шовкун answer is the right one but if you did want to use the model attribute notation you could get the required argument into the model via a class variable.
class YourModel extends Model{
public $code;
public function getProductAttribute()
{
//a more eloquent way to get the required attribute
if($attribute = $this->productAttributes->filter(function($attribute){
return $attribute->code = $this->code;
})->first()){
return $attribute->pivot->value;
}
return null;
}
}
Then do
$model->code = 'color';
echo $model->product;
But its a bit long and pointless

Laravel 5 Global CRUD Class

Before anyone asks, I've looked into CRUD generators and I know all about the Laravel Resource routes, but that's not exactly what I'm pulling for here.
What I'm looking to do is create one Route with a couple parameters, and one global class that (uses/extends?) the Model controller for simple CRUD operations. We have 20 or so Models and creating a Resource Controller for each table would be more time consuming than finding a way to create a global CRUD class to handle all "api" type calls and any ajax json request like a create / update / destroy statement.
So my question is what is the cleanest and best way to structure a class to handle all CRUD requests for every Model we have without having to have a resource controller for every model? I've tried researching this and can't seem to find any links except ones to CRUD generators and links describing the laravel Resource route.
The easiest way would be to do the following:
Add a route for your resource controller:
Route::resource('crud', 'CrudController', array('except' => array('create', 'edit')));
Create your crud controller
<?php namespace App\Http\Controllers;
use Illuminate\Routing\Controller;
use App\Models\User;
use App\Models\Product;
use Input;
class CrudController extends Controller
{
const MODEL_KEY = 'model';
protected $modelsMapping = [
'user' => User::class,
'product' => Product::class
];
protected function getModel() {
$modelKey = Input::get(static::MODEL_KEY);
if (array_key_exists($modelKey, $this->modelsMapping)) {
return $this->modelsMapping[$modelKey];
}
throw new \InvalidArgumentException('Invalid model');
}
public function index()
{
$model = $this->getModel();
return $model::all();
}
public function store()
{
$model = $this->getModel();
return $model::create(array_except(Input::all(), static::MODEL_KEY));
}
public function show($id)
{
$model = $this->getModel();
return $model::findOrFail($id);
}
public function update($id)
{
$model = $this->getModel();
$object = $model::findOrFail($id);
return $object->update(array_except(Input::all(), static::MODEL_KEY));
}
public function destroy($id)
{
$model = $this->getModel();
return $model::remove($id);
}
}
Use your new controller :) You have to pass the model parameter that will contain the model key - it must be one of the allowed models in the whitelist. E.g. if you want to get a User with id=5 do
GET /crud/5?model=user
Please keep in mind that it's as simple as possible, you might need to make the code more sophisticated to match your needs.
Please also keep in mind that this code has not been tested - let me know if you see any typos or have some other issues. I'll be more than happy to get it running for you.
Unless you want to implement CRUD manually, consider to integrate a ready-made datagrid such as phpGrid.
Check out integration walkthrough: http://phpgrid.com/example/phpgrid-laravel-5-twitter-bootstrap-3-integration/ No models are required and the code is minimum. It can almost do anything.
A basic working CRUD:
// in a controller
public function index()
{
$dg = new \C_DataGrid("SELECT * FROM orders", "orderNumber", "orders");
$dg->enable_edit("FORM", "CRUD");
$dg->display(false);
$grid = $dg -> get_display(true);
return view('dashboard', ['grid' => $grid]);
}
You need one generic class for all CRUD operations and there are many ways to achieve that and one rule for all may not fit but you may try the approach that I'm going to describe now. This is an abstract idea, you need to implement it, so at first, think the URI for all CRUD operations. In this case you must follow a convention and it could be something like this:
example.com/user/{id?} // get all or one by id (if id is available in the URI)
example.com/user/create // Show an empty form
example.com/user/edit/10 // Show a form populated with User model
example.com/user/save // Create a new User
example.com/user/save/10 // Update an existing User
example.com/user/delete/10 // Delete an existing User
In ths case the user could be something else to specify the name of the model for example, example.com/product/create and keeping that on mind, you need to declare routes as given below:
Route::get('/{model}/{id?}', 'CrudController#read');
Route::get('/{model}/create', 'CrudController#create');
Route::get('/{model}/edit/{id}', 'CrudController#edit');
Route::post('/{model}/save/{id?}', 'CrudController#save');
Route::post('/{model}/delete/{id}', 'CrudController#delete');
Now, in your app\Providers\RouteServiceProvider.php file modify the boot method and make it look like this:
public function boot(Router $router)
{
$model = null;
$router->bind('model', function($modelName) use (&$model, &$router)
{
$model = app('\App\User\\'.ucfirst($modelName));
if($model)
{
if($id = $router->input('id'))
{
$model = $model->find($id);
}
return $model ?: abort(404);
}
});
parent::boot($router);
}
Then declare your CrudController as given below:
class CrudController extends Controller
{
protected $request = null;
public function __construct(Request $request)
{
$this->request = $request;
}
public function read($model)
{
return $model->exists ? $model : $model->all();
}
// Show either an empty form or a form
// populated with the given model atts
public function createOrEdit($model)
{
$classNameArray = explode('\\', get_class($model));
$className = strtolower(array_pop($classNameArray));
$view = view($className . '.form');
$view->formAction = "$className/save";
if(is_object($model) && $model->exists)
{
$view->model = $model;
$view->formAction .= "/{$model->id}";
}
return $view;
}
public function save($model)
{
// Validation required so do it
// Make sure each Model has $fillable specified
return $this->model->fill($this->request)->save();
}
public function delete($model)
{
return $this->model->delete();
}
}
Since same form is used to creating and updating a model, use something like this to create a form:
<form action="{{url($formAction)}}" method="POST">
<input
type="text"
class="form-control"
name="first_name" value="{{old('first_name', #$model->first_name)}}"
/>
<input type="Submit" value="Submit" />
{!!csrf_field()!!}
</form>
Remember that, each form should be in a directory corresponding to the model, for user add/edit, form should be in views/user/form.blade.php and for product model use views/product/form.blade.php and so on.
This will work and don't forget to add validation before saving a model and validation could be done inside the model using model events or however you want. This is just an idea but probably not the best way to it.

Yii, several instances of model in one form

We have two models
class Base extends AbstractModel
{
public $id;
public $name;
public function tableName()
{
return 'table_base';
}
}
class ExtendBase extends AbstractModel
{
public $id;
public $baseID;
public function tableName()
{
return 'table_base_extend';
}
}
I won't describe all, will describe only main things.
So, now I want create form that will combine this forms. And want in action do something like this
public function actionSome ()
{
$oBase = new Base();
$aExtendBase = array(); // important this is array
for ($i = 1; $i <= 3; $i++) {
$aExtendBase[] = new Extend(); // fill in array, pre-create for form
}
if ($this->isPost()) { // protected custom method
if (isset($_POST['Base'], $_POST['ExtendBase'])) // here should be specific checker because we have several ExtendBase instances in form
{
// save Base model
// save each ExtendBase model
}
}
$this->render('formView', array('oBase' => $oBase, 'aExtendBase' => $aExtendBaes))
}
So question is
How to create such 'combined' form in view;
How to get all forms data (I mean each) after POST action;
See the solution on the Yii WIKI
http://www.yiiframework.com/wiki/19/how-to-use-a-single-form-to-collect-data-for-two-or-more-models/
You can create a model (CFormModel) which does not represent an ActiveRecord just for a single form.
I think it would be the best way for you to proceed (assuming I've understood your problem correctly).
If I were you, I would create a Model to represent the rules for this form only (like I said, an instance of CFormModel, not ActiveRecord) and then, in your controller, take the elements that got submitted and manually create and/or update objects for both classes.

Laravel update or create an Item

I have this question about Laravel:
I have a my model and my RestfulAPI controller.
Into the store() method I would check if I have an element that already has the field 'myField' (myField id different from 'id') equal to what I have to create. If it already exist then I would like to update, otherwise I would simply create (save())..
Have I to use find() method?
From my experience, you'll have to traverse table and check for uniqueness.
You can create your helper function and use something like array_unique function. Maybe it is worth checking how Validator class is checking that users entry is unique.
Currently we have firstOrCreate or firstOrNew, but I don't think they really fit your needs. For instance, firstOrCreate will try to locate a row by all attributes, not just some, so an update in this case wouldn't make sense. So I think you really would have to find it, but you can create a BaseModel and create a createOrUpdate method that could look like this:
This is untested code
class BaseModel extends Eloquent {
public function createOrUpdate($attributes, $keysToCheck = null)
{
// If no attributes are passed, find using all
$keysToCheck = $keysToCheck ?: $attributes;
if ($model = static::firstByAttributes(array_only($keysToCheck, $attributes))
{
$model->attributes = $attributes;
$model->save();
}
else
{
$model = static::create($attributes);
}
return $model;
}
}
This is an implementation of it:
class Post extends BaseModel {
public function store()
{
$model = $this->createOrUpdate(Input::all(), ['full_name']);
return View::make('post.created', ['model' => $model]);
}
}

Add a custom attribute to a Laravel / Eloquent model on load?

I'd like to be able to add a custom attribute/property to an Laravel/Eloquent model when it is loaded, similar to how that might be achieved with RedBean's $model->open() method.
For instance, at the moment, in my controller I have:
public function index()
{
$sessions = EventSession::all();
foreach ($sessions as $i => $session) {
$sessions[$i]->available = $session->getAvailability();
}
return $sessions;
}
It would be nice to be able to omit the loop and have the 'available' attribute already set and populated.
I've tried using some of the model events described in the documentation to attach this property when the object loads, but without success so far.
Notes:
'available' is not a field in the underlying table.
$sessions is being returned as a JSON object as part of an API, and therefore calling something like $session->available() in a template isn't an option
The problem is caused by the fact that the Model's toArray() method ignores any accessors which do not directly relate to a column in the underlying table.
As Taylor Otwell mentioned here, "This is intentional and for performance reasons." However there is an easy way to achieve this:
class EventSession extends Eloquent {
protected $table = 'sessions';
protected $appends = array('availability');
public function getAvailabilityAttribute()
{
return $this->calculateAvailability();
}
}
Any attributes listed in the $appends property will automatically be included in the array or JSON form of the model, provided that you've added the appropriate accessor.
Old answer (for Laravel versions < 4.08):
The best solution that I've found is to override the toArray() method and either explicity set the attribute:
class Book extends Eloquent {
protected $table = 'books';
public function toArray()
{
$array = parent::toArray();
$array['upper'] = $this->upper;
return $array;
}
public function getUpperAttribute()
{
return strtoupper($this->title);
}
}
or, if you have lots of custom accessors, loop through them all and apply them:
class Book extends Eloquent {
protected $table = 'books';
public function toArray()
{
$array = parent::toArray();
foreach ($this->getMutatedAttributes() as $key)
{
if ( ! array_key_exists($key, $array)) {
$array[$key] = $this->{$key};
}
}
return $array;
}
public function getUpperAttribute()
{
return strtoupper($this->title);
}
}
The last thing on the Laravel Eloquent doc page is:
protected $appends = array('is_admin');
That can be used automatically to add new accessors to the model without any additional work like modifying methods like ::toArray().
Just create getFooBarAttribute(...) accessor and add the foo_bar to $appends array.
If you rename your getAvailability() method to getAvailableAttribute() your method becomes an accessor and you'll be able to read it using ->available straight on your model.
Docs: https://laravel.com/docs/5.4/eloquent-mutators#accessors-and-mutators
EDIT: Since your attribute is "virtual", it is not included by default in the JSON representation of your object.
But I found this: Custom model accessors not processed when ->toJson() called?
In order to force your attribute to be returned in the array, add it as a key to the $attributes array.
class User extends Eloquent {
protected $attributes = array(
'ZipCode' => '',
);
public function getZipCodeAttribute()
{
return ....
}
}
I didn't test it, but should be pretty trivial for you to try in your current setup.
I had something simular:
I have an attribute picture in my model, this contains the location of the file in the Storage folder.
The image must be returned base64 encoded
//Add extra attribute
protected $attributes = ['picture_data'];
//Make it available in the json response
protected $appends = ['picture_data'];
//implement the attribute
public function getPictureDataAttribute()
{
$file = Storage::get($this->picture);
$type = Storage::mimeType($this->picture);
return "data:" . $type . ";base64," . base64_encode($file);
}
Step 1: Define attributes in $appends
Step 2: Define accessor for that attributes.
Example:
<?php
...
class Movie extends Model{
protected $appends = ['cover'];
//define accessor
public function getCoverAttribute()
{
return json_decode($this->InJson)->cover;
}
you can use setAttribute function in Model to add a custom attribute
Let say you have 2 columns named first_name and last_name in your users table and you want to retrieve full name. you can achieve with the following code :
class User extends Eloquent {
public function getFullNameAttribute()
{
return $this->first_name.' '.$this->last_name;
}
}
now you can get full name as:
$user = User::find(1);
$user->full_name;
In my subscription model, I need to know the subscription is paused or not.
here is how I did it
public function getIsPausedAttribute() {
$isPaused = false;
if (!$this->is_active) {
$isPaused = true;
}
}
then in the view template,I can use
$subscription->is_paused to get the result.
The getIsPausedAttribute is the format to set a custom attribute,
and uses is_paused to get or use the attribute in your view.
in my case, creating an empty column and setting its accessor worked fine.
my accessor filling user's age from dob column. toArray() function worked too.
public function getAgeAttribute()
{
return Carbon::createFromFormat('Y-m-d', $this->attributes['dateofbirth'])->age;
}

Categories