Yii, several instances of model in one form - php

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.

Related

How can I make a specific function on action page to handle the form data?

I have a models.php page that contains the specification of form for a specific model.
models.php
$books = [
['Book Name', 'text' ],
['Author', 'text']
];
$vegetables = [
['Name', 'text'],
['Photo', 'file']
]
Now this page is accessed by an admin.php page, which generate an appropriate HTML form on the basis of the given name and input type.
I want to fill the form and send the data into a handle.php and handle the data with the specific function to fill the data into appropriate table.
handle.php
function books(){
// this will fill the details into table of books.
INSERT INTO BOOKS
name = $_POST['book_name']
author $_POST['author']
}
function vegetables(){
// this will fill the details into table of vegetables.
INSERT INTO VEGETABLES
name = $_POST['book_name']
photo = $_FILE['photo']
}
(If there's any other better way of doing this, so please mention, I'll do that way and delete my question.)
Here's my suggestion. As stated in the comments, this is just my way to do such things, it's not necessarily the best solution for every situation.
I have a base model, that defines all methods all model need to have in common. Here's a very simplified version:
class Model {
public $modelName = 'default';
public $id = null;
private $fields = [];
private $tableName = 'default';
private $tableDefinition = [];
private $idField = 'id';
public function insert($dataset) {
// do some database magic by using $this->fields, or $this->tableDefinition
$sql = "INSERT into {$this->tableName} ...";
...
return $id;
}
public function update($id, $dataset) {
// do some more database magic by using $this->fields, or $this->tableDefinition
}
// many more methods. To get data, delete, sort, ..
//...
}
Every model now extends this base model class and sets it's specific params, maybe even overrides some methods or adds special ones:
class Books extends Model {
public $modelName = 'book';
private $fields = ['bookName','Author'];
private $tableName = 'BOOKS';
private $tableDefinition = [
['bookName','varchar'],
['Author','varchar']
];
// private $idField = 'id'; // you can ommit that, if it's the default.
}
If Vegetables behaves different you can simply override a method:
class Vegetables extends Model {
public $modelName = 'vegetable';
// set all other properties...
// override insert() for example
public function insert($dataset) {
// do something that doesn't comply with the standard procedure
}
}
Then in handle.php you can do something like this:
<?php
$modelName = $request; // get it from your form, your url, ..
// & verify this model(file) exists.
$model = new $modelName();
$model->insert($dataSet);
Make a base interface BaseModel.php
which would have the basic signatures of insertion , updation and selection
Make a derived class booksModel.php and vegetablesModel.php that would implement the BaseModel class.
In this way, you have made your code extendable. If there is some common functionality, you can make the base class as Abstract class.
abstract class BaseModel {
abstract function add($dataObject);
abstract function get($dataObject);
}
class BooksModel extends BaseModel {
public function add($dataObject) {
/* Implementation */
}
public function get($dataObject) {
/* Implementation */
}
}
class VegetableModel extends BaseModel {
public function add($dataObject) {
/* Implementation */
}
public function get($dataObject) {
/* Implementation */
}
}

Yii2. Adding attribute and rule dynamically to model

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?

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.

Laravel - Using controller function for two different eloquent models

I am currently developing an application for an indy movie production company. The way I have the workflow right now, the user begins by creating a new movie object by entering the movie title and synopsis. From there the user can then add more details such as price, run-time, full-screen/wide-screen, etc. The movie basic (title, synopsis) are in one database table, and the details are in another. I have set up a one-to-one relationship between the two eloquent models. I have also set up a MovieController that allows me to very easily do CRUD operations on the movie basic model, and when I am displaying the movie object to the user, I can display both the basics and details.
What I was wondering was there some way to use the already existent functions in the movie controller to do CRUD operations on the movie details without having to create new functions in the controller? Also is it possible to reuse the views I've created for each corresponding CRUD operation? In other words can I would like
something.dev/cms/create
In one instance to match to creating a new movie (title, synopsis) and in another instance to match to creating the movie detail (price, run-time, full-screen/widescreen) etc. Is this possible? I have provide the code for the two models below:
Movie_basic.php
<?php namespace App;
use Illuminate\Database\Eloquent\Model;
class Movie_basic extends Model {
protected $fillable = ['movie_title', 'movie_synopsis'];
protected $guarded = ['id'];
public function details()
{
return $this->hasOne('App\Movie_detail', 'movie_id');
}
public function personnel()
{
return $this->hasMany('App\Movie_personnel', 'movie_id');
}
}
Model_detail.php
<?php namespace App;
use Illuminate\Database\Eloquent\Model;
class Movie_detail extends Model {
protected $fillable = ['minutes', 'languages', 'viewer_discretion', 'screen_type', 'price'];
protected $guarded = ['id', 'movie_id'];
public function basics()
{
return $this->belongsTo('App\Movie_basic');
}
}
If I understand you, this might be an answer. (Did not test the code.)
Please note that, that code has been written to show you an example. You will probably want to edit it to make it work and act as you wanted. Maybe you want to use a repository or automate the model instance creating (I did not create new instances), and saving processes. You can use interfaces instead of your models etc...
Here is the service to store the logic.
<?php
use Movie_basic; use Movie_detail;
Class MovieService {
protected $movieBasic;
protected $movieDetail;
public function __construct(Movie_basic $movieBasic, Movie_detail $movieDetail) {
$this->movieBasic = $movieBasic;
$this->movieDetail = $movieDetail;
}
public function createMovie(array $attr) {
// TODO: Move your business logic here.
// E.g
$movie = $this->movieBasic->fill($attr);
$movie->save();
return $movie;
}
public function createMovieDetail(array $movieAttr, array $attributes) {
// TODO: Move your detail logic here.
// E.g.
$basic = $this->createMovie($movieAttr);
$detail = $this->movieDetail->fill($attributes);
$detail->basic()->associate($detail);
$detail->save();
return $detail;
}
}
And here, the controller examples:
<?php
use MovieService;
class MovieController {
public function __construct(MovieService $ms) {
$this->ms = $ms;
}
public function store() {
$this->ms->createMovie($attrToSave);
}
}
<?php
use MovieService;
class MovieDetailController {
public function __construct(MovieService $ms) {
$this->ms = $ms;
}
public function store() {
$this->ms->createMovieDetail($attrToSave);
}
}

Domain Object that needs more than one Data Mapper

I'm trying to figure out how to reuse Domain Models in different parts of the application and I have a feeling that the Data Mapper pattern is the way forward. The example below has methods that directly access the methods of the Mapper.
class Groups
{
protected $_groups = array();
public function addGroup($name)
{
$this->_groups[] = $name;
}
public function doSomethingGroupy($cakes)
{
// get all the groups that have cake
return $cakeyGroups;
}
}
... And a mapper to match the methods on the Groups class.
class GroupMapper
{
public function find($id, Groups $group)
{
// Mappy type things, maybe some sql
}
public function fetchByNeediness($cuddles, Groups $group)
{
// More mappy type things
}
public function save(Groups $groups)
{
// Saves
}
}
However if sometime later I wanted to use the same Groups Models but populate the groups using different queries I would use a different mapper.
class AngryGroupMapper
{
public function find($id, Groups $group)
{
// Something similar but with other tables and joins
}
public function fetchByRage($anger, Groups $group)
{
// Something new but only needed here
}
public function isEditable(Groups $groups)
{
// Do some querying
return $bool;
{
}
Now I Know the aim is Skinny Controller - Fat Model, so would I have another model to Map the Mapper (so to speak) to the Model?
class FatModelRepository
{
public function getHappyGroups()
{
$mapper = new GroupMapper();
return $mapper->fetchByNeediness('Puffy Shoes', new Groups());
}
public function getSadGroups()
{
$mapper = new AngryGroupMapper();
return $mapper->fetchByRage('Aghh!', new Groups());
{
public function save(Groups $groups)
{
$mapper = new GroupMapper();
return $mapper->save($groups);
{
}
The Data Model should have no knowledge of the Data Mapper. Your Groups class/model shouldn't have find methods and it should not have access to the mapper.
Once you remove the mapper dependency from your model your problems will go away.
NOTE: check out Doctrine 2
As rojoca says you shouldnt have the fetch/find methods directly on the model. Technically hes also right about the model not storing a reference to the mapper, but in less complex situations i think this is ok so long as the model only excpets the most abstract form of mapper you plan on having (ie. some kind of base mapper class or an interface).
Given thos to things, you should only need to add methods to the mapper, and for this i would just use inheritance, ie. extend your groups mapper for the new functionality. Of course this requires that the mapper is injectable into the model. But if youre going to have the model hold a reference to its mapper then it does need to be injectable anyhow.

Categories