Yii2 rest save multiple models - php

Using a REST approach I want to be able to save more than one model in a single action.
class MyController extends ActiveController {
public $modelClass = 'models\MyModel';
}
class MyModel extends ActiveRecord {
...
}
That automagically creates actions for a REST api. The problem is that I want to save more than one model, using only that code in a POST will result in a new record just for MyModel. What if I need to save AnotherModel?
Thanks for any suggestion.

ActiveController implements a common set of basic actions for supporting RESTful access to ActiveRecord. For more advanced use you will need to override them or just merge to them your own custom actions where you will be implementing your own code & logic.
Check in your app the /vendor/yiisoft/yii2/rest/ folder to see how ActiveController is structured and what is doing each of its actions.
Now to start by overriding an ActiveController's action by a custom one, you can do it within your controller. Here is a first example where i'm overriding the createAction:
1-
class MyController extends ActiveController
{
public $modelClass = 'models\MyModel';
public function actions()
{
$actions = parent::actions();
unset($actions['create']);
return $actions;
}
public function actionCreate(){
// your code
}
}
2-
Or you can follow the ActiveController's structure which you can see in /vendor/yiisoft/yii2/rest/ActiveController.php by placing your custom actions in separate files. Here is an example where I'm overriding the updateAction by a custom one where i'm initializing its parameters from myController class :
class MyController extends ActiveController
{
public $modelClass = 'models\MyModel';
public function actions() {
$actions = parent::actions();
$custom_actions = [
'update' => [
'class' => 'app\controllers\actions\WhateverAction',
'modelClass' => $this->modelClass,
'checkAccess' => [$this, 'checkAccess'],
'scenario' => $this->updateScenario,
'params' => \Yii::$app->request->bodyParams,
],
];
return array_merge($actions, $custom_actions);
}
}
Now let's say as example that in my new action file app\controllers\actions\WhateverAction.php I'm expecting the Post Request (which i'm storing in $params) to have a subModels attribute storing a list of child models to which I'm going to apply some extra code like relating them with their parent model if they already exists in first place :
namespace app\controllers\actions;
use Yii;
use yii\base\Model;
use yii\db\ActiveRecord;
use yii\web\ServerErrorHttpException;
use yii\rest\Action;
use app\models\YourSubModel;
class WhateverAction extends Action
{
public $scenario = Model::SCENARIO_DEFAULT;
public $params;
public function run($id)
{
$model = $this->findModel($id);
if ($this->checkAccess) {
call_user_func($this->checkAccess, $this->id, $model);
}
$model->scenario = $this->scenario;
$model->load($this->params, '');
foreach ($this->params["subModels"] as $subModel) {
/**
* your code related to each of your model's posted child
* for example those lines will relate each child model
* to the parent model by saving that to database as their
* relationship has been defined in their respective models (many_to_many or one_to_many)
*
**/
$subModel = YourSubModel::findOne($subModel['id']);
if (!$subModel) throw new ServerErrorHttpException('Failed to update due to unknown related objects.');
$subModel->link('myParentModelName', $model);
//...
}
// ...
return $model;
}
}

So if I understand you wish to add a new database entry not only for the model you are querying, but for another model.
The best place to do this would be in the AfterSave() or BeforeSave() functions of the first model class. Which one would depend on the data you are saving.

Related

Is there a way to decouple Laravel Eloquent models from Service or Controller level?

There is a simple Laravel Eloquent Model below:
use Illuminate\Database\Eloquent\Model;
class Product extends Model
{
}
and it's normal to use repository pattern to work with model, like:
use Product;
class ProductRepository implement ProductRepositoryInterface
{
public function __construct(Product $model)
{
$this->model = $model;
}
public function findById($id)
{
return $this->model->find($id);
}
...
}
The controller use the repository to get Prodcut data:
class ProductController extends Controller
{
private $productRepository;
public function __construct(ProductRepository $productRepository)
{
$this->productRepository = $productRepository;
}
public function getSomeInfoOfProduct($id)
{
$product = $this->productRepository->findById($id);
return [
'name' => $product->name,
'alias' => $product->alias,
'amount' => $product->amount,
];
}
}
In the method getSomeInfoOfProduct, when I am deciding what kind of information should I return, I don't know there are how many properties the $product object has until I look at the schema of table products or migration files.
It's look like that the controller is tightly coupled with Eloquent models and the database. If one day, I store the raw data of products in Redis or other places, I still need to create a Eloquent model object, and fill in the object with the data from Redis.
So I am considering to create a pure data object to replace the Eloquent Model object, like below:
class ProductDataObject
{
private $name;
private $alias;
private $amount;
private $anyOtherElse;
public function getName() {
return $this->name;
}
....
}
and let the repository return this object:
use Product;
use ProductDataObject;
class ProductRepository implement ProductRepositoryInterface
{
public function __construct(Product $model)
{
$this->model = $model;
}
public function findById($id)
{
$result = $this->model->find($id);
// use some way to fill properties of the object
return new ProductDataObject(...);
}
...
}
In the controller or service level, I can just look at ProductDataObject to get all information I need. And it also looks like easier to change data storage without affecting the controllers and services.
Does this way make sense?
I think what you're looking for is the Factory Pattern. You're kind of on the right track already. Basically you have a middle-man class that your Controller or Repository basically asks to supply them with the appropriate Model. Through either parsing conditions or a config file using .envs, it figures out which one to serve up, so long as anything it returns all implements the same Interface.

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.

Updating timestamps on attaching/detaching Eloquent relations

I'm using Laravel 4, and have 2 models:
class Asset extends \Eloquent {
public function products() {
return $this->belongsToMany('Product');
}
}
class Product extends \Eloquent {
public function assets() {
return $this->belongsToMany('Asset');
}
}
Product has the standard timestamps on it (created_at, updated_at) and I'd like to update the updated_at field of the Product when I attach/detach an Asset.
I tried this on the Asset model:
class Asset extends \Eloquent {
public function products() {
return $this->belongsToMany('Product')->withTimestamps();
}
}
...but that did nothing at all (apparently). Edit: apparently this is for updating timestamps on the pivot table, not for updating them on the relation's own table (ie. updates assets_products.updated_at, not products.updated_at).
I then tried this on the Asset model:
class Asset extends \Eloquent {
protected $touches = [ 'products' ];
public function products() {
return $this->belongsToMany('Product');
}
}
...which works, but then breaks my seed which calls Asset::create([ ... ]); because apparently Laravel tries to call ->touchOwners() on the relation without checking if it's null:
PHP Fatal error: Call to undefined method Illuminate\Database\Eloquent\Collection::touchOwners() in /projectdir/vendor/laravel/framework/src/Illuminate/Database/Eloquent/Model.php on line 1583
The code I'm using to add/remove Assets is this:
Product::find( $validId )->assets()->attach( $anotherValidId );
Product::find( $validId )->assets()->detach( $anotherValidId );
Where am I going wrong?
You can do it manually using touch method:
$product = Product::find($validId);
$product->assets()->attach($anotherValidId);
$product->touch();
But if you don't want to do it manually each time you can simplify this creating method in your Product model this way:
public function attachAsset($id)
{
$this->assets()->attach($id);
$this->touch();
}
And now you can use it this way:
Product::find($validId)->attachAsset($anotherValidId);
The same you can of course do for detach action.
And I noticed you have one relation belongsToMany and the other hasMany - it should be rather belongsToMany in both because it's many to many relationship
EDIT
If you would like to use it in many models, you could create trait or create another base class that extends Eloquent with the following method:
public function attach($id, $relationship = null)
{
$relationship = $relationship ?: $this->relationship;
$this->{$relationship}()->attach($id);
$this->touch();
}
Now, if you need this functionality you just need to extend from another base class (or use trait), and now you can add to your Product class one extra property:
private $relationship = 'assets';
Now you could use:
Product::find($validId)->attach($anotherValidId);
or
Product::find($validId)->attach($anotherValidId, 'assets');
if you need to attach data with updating updated_at field. The same of course you need to repeat for detaching.
From the code source, you need to set $touch to false when creating a new instance of the related model:
Asset::create(array(),array(),false);
or use:
$asset = new Asset;
// ...
$asset->setTouchedRelations([]);
$asset->save();
Solution:
Create a BaseModel that extends Eloquent, making a simple adjustment to the create method:
BaseModel.php:
class BaseModel extends Eloquent {
/**
* Save a new model and return the instance, passing along the
* $options array to specify the behavior of 'timestamps' and 'touch'
*
* #param array $attributes
* #param array $options
* #return static
*/
public static function create(array $attributes, array $options = array())
{
$model = new static($attributes);
$model->save($options);
return $model;
}
}
Have your Asset and Product models (and others, if desired) extend BaseModel rather than Eloquent, and set the $touches attribute:
Asset.php (and other models):
class Asset extends BaseModel {
protected $touches = [ 'products' ];
...
In your seeders, set the 2nd parameter of create to an array which specifies 'touch' as false:
Asset::create([...],['touch' => false])
Explanation:
Eloquent's save() method accepts an (optional) array of options, in which you can specify two flags: 'timestamps' and 'touch'. If touch is set to false, then Eloquent will do no touching of related models, regardless of any $touches attributes you've specified on your models. This is all built-in behavior for Eloquent's save() method.
The problem is that Eloquent's create() method doesn't accept any options to pass along to save(). By extending Eloquent (with a BaseModel) to accept the $options array as the 2nd attribute, and pass it along to save(), you can now use those two options when you call create() on all your models which extend BaseModel.
Note that the $options array is optional, so doing this won't break any other calls to create() you might have in your code.

How to get a model by id when injecting a model to a controller?

class LessonController extends \BaseController {
protected $lesson;
public function __construct(\Lesson $lesson)
{
}
public function edit($lesson)
{
var_dump($this->lesson);
}
}
Here how can I var_dump the selected model based on the user going to a route like domain.com/lesson/edit/{id}?
What you're injecting isn't an instance of the model, but rather the class that provides access to instances. Your calls will look like un-injected calls but with $this->lesson replacing Lesson::.
To find a particular instance, then, you'll call
$lessonInstance = $this->lesson->find($id); // if not injected, would be Lesson::find($id)
var_dump($lessonInstance);

Symfony2 / FOSUserBundle: Change render variables in response without altering parent class

One of my classes currently extends the BaseController on the FOSUserBundle, and returns the parent action. However, due to project spec, I shouldn't have the need to edit the parent class. Is there a way of sending additional variables, for twig to render, through the child response?
Child Class:
class ChangePasswordController extends BaseController
{
public function changePasswordAction(Request $request)
{
$response = parent::changePasswordAction($request);
return $response; // and 'myVariable' => $myVariable
}
}
Parent Class:
class ChangePasswordController extends ContainerAware
{
/**
* Change user password
*/
public function changePasswordAction(Request $request)
{
//lots of code.....
return $this->container->get('templating')
->renderResponse(
'FOSUserBundle:ChangePassword:changePassword.html.'
.$this->container->getParameter('fos_user.template.engine'),
array(
'form' => $form->createView()
//and 'myVariable' => $myVariable
)
);
}
}
So to summarise, is there a way of passing something to the parent class, without changing the parent class... whilst rendering the twig view with an additional variable.
-- Update --
Essentially I want to render a form using the FOSUserBundle changePassword action, therefore this works fine:
return $this->container
->get('templating')
->renderResponse(
'FOSUserBundle:ChangePassword:changePassword.html.'.$this->container->getParameter('fos_user.template.engine'),
array('form' => $form->createView())
);
However, I want to pass more variables to the view, just like the 'form' is passed as shown above, without altering the FosUserBundle ChangePassword Controller. Therefore I have a class which inherits the that controller, adds some additional functionality and returns the parent change password action:
class ChangePassController extends ChangePasswordController
{
public function changePasswordAction(Request $request)
{
// more code......
$response = parent::changePasswordAction($request);
return $response;
}
}
But, like with most applications, I want to add more than just the form variable to a view template. So is there a way of passing an additional variable to the view, without altering the parent controller / action? Like (but not like) pushing 'myVariable' => $myVariable to the parent changePasswordAction return statement?
There is a section in FOSUserBundle documentation that describes exactly how to do that, and from Symfony2's Cookbook, How to use Bundle Inheritance to Override parts of a Bundle.
In summary, create a Bundle class to override FOSUserBundle in src:
// src/Acme/UserBundle/AcmeUserBundle.php
<?php
namespace Acme\UserBundle;
use Symfony\Component\HttpKernel\Bundle\Bundle;
class AcmeUserBundle extends Bundle
{
public function getParent()
{
return 'FOSUserBundle';
}
}
Then, override the ChangePasswordController class:
use FOS\UserBundle\Controller\ChangePasswordController as BaseController;
class ChangePasswordController extends BaseController
{
public function changePasswordAction(Request $request)
{
$response = parent::changePasswordAction($request);
return $response; // and 'myVariable' => $myVariable
}
}
--UPDATE--
Ok I think I misread you question. Anyway what renderResponse() of the templating service does is essentially:
$response->setContent($this->render($view, $parameters));
You can see the Class of the templating service by running app/console container:debug which is actually the TwigEngine class.
So you can just re-invoke renderResponse() and supply you own extra parameters. eg:
return $this->container->get('templating')->renderResponse(
'FOSUserBundle:ChangePassword:changePassword.html.'.$this->container->getParameter('fos_user.template.engine'),
array(
'form' => $form->createView(),
'myVariable' => $myVariable', // There you go
),
$response // The previous response that has been rendered by the parent class, by this is not necessary
);
Think bottom up.
You can access your data without passing it through action, using Twig Extension http://symfony.com/doc/current/cookbook/templating/twig_extension.html
twig.extension.user_profile:
class: 'MyBundle\UserProfileExtension'
arguments:
- '#doctrine.orm.entity_manager'
tags:
- { name: twig.extension }
Extension class
class UserProfileExtension extends \Twig_Extension
{
/**
* #var EntityManager
*/
private $entityManager;
/**
* #param UserProfileDataService $userProfileDataService
*/
public function __construct(EntityManager $entityManager)
{
$this->entityManager = $entityManager;
}
/**
* #return array
*/
public function getFunctions()
{
return array(
new \Twig_SimpleFunction('get_my_custom_var', array($this, 'getMyCustomVar')),
);
}
/**
* #return array
*/
public function getMyCustomVar()
{
$var = $this->entityManager->getRepository('MyCustomRepository')->findOneBy(['id' => 1]);
return $var;
}
/**
* Returns the name of the extension.
*
* #return string The extension name
*/
public function getName()
{
return 'user_profile_extension';
}
Template usage
{dump(get_my_custom_var())}
if I am understanding your question correctly you should be able to set additional variables on the response like this:
use FOS\UserBundle\Controller\ChangePasswordController as BaseController;
class ChangePasswordController extends BaseController
{
public function changePasswordAction(Request $request)
{
$response = parent::changePasswordAction($request);
$response['myVariable'] = $myVariable;
return $response;
}
}
Hope this helps!

Categories