Yii2: Post complex models - php

I have a complex entity, looking similar like this:
class Article extends \yii\db\ActiveRecord {
public $id;
public $name;
/** #var ArticleAspectValue[] */
public $aspects;
public function getArticleAspectValues() {
return $this->hasMany(ArticleAspectValue::className(), ['article_id' => $this->id]);
}
}
And I have an entity serving as part of the more complex entity above.
class ArticleAspectValue extends \yii\db\ActiveRecord {
public $aspect_id; // <--- Two-attributes identifier (two-col PK in db)
public $article_id; // <----'
public $value;
}
While every ArticleAspectValue is assigned to ArticleAspect and Article as well, one article only has its own ArticleAspectValues.
The model Article consists of an id, a name and an array of sub-entities called ArticleAspectValues.
I solved the creation of input fields for each ArticleAspectValue, but since this is a simple for-each on the frontend with no connection to the model behind.
Question: How has the form and the receiving controller method to look like in order to post new values on the sub-entities, but according to their superior model, the Article?
PS the doc on complex models is TBD

The solution is to save the inferior model in the controller method as well as the superior, if they are ActiveRecords as well.
Following the appreciated comment from Mihai P., I repost my corrected example code from above and the solution.
The major model of the complex structure looks like this:
/**
* The superior class in a complex model.
* #property int $id
* #property string $name
*
* #property ArticleAspectValues $aspects
*/
class Article extends \yii\db\ActiveRecord
{
/**
* #return \yii\db\ActiveQuery
*/
public function getArticleAspectValues() {
return $this->hasMany(ArticleAspectValue::className(), ['article_id' => 'id']);
}
}
In Yii, properties of an ActiveRecord are created of database table columns. Yii provides magic getters to obtain their values. To work with properties which aren't actually existing, you can annotate them in the class. Most IDE's would parse these annotations and provide them as regular items of a class. Same works for methods.
The inferior class of the complex model looks like this:
/**
* The inferior class of the complex model.
* #property int $aspect_id
* #property int $article_id
* #property string $value
*/
class ArticleAspectValue extends \yii\db\ActiveRecord
{
/**
* #return \yii\db\ActiveQuery
*/
public function getArticle() {
return $this->hasOne(Article::className(), ['article_id' => 'id]);
}
}
In the controller of the superior model, the inferiors are saved within at the same time.
class ArticleController extends \yii\db\ActiveRecord
{
// ...
// Exemplary method. Goes for create action as well.
public function actionUpdate($id) {
$model = $this->findModel($id);
if ($model->load(Yii::$app->request->post()) && $model->save()) {
if ($model->aspects->load(Yii::$app->request->post()) && $model->aspects->save()) {
return $this->redirect(['view', 'id' => $model->id]);
}
}
}
The load method of ActiveRecords is able to parse a whole post data body to obtain its own values.
It's probably better to extend load and save methods of your complex model to process the inferiors, rather than instruct the controller for each inferior model.

Related

Abstract factory class returning an interface type, method not found?

I am attempting to use the abstract factory pattern. I've created a class, FactoryProducer, that creates a class-specific factory based on a string passed into one of the two class methods.
The issue I'm having is that I've extended one of the concrete factory classes, but the FactoryProducer returns an interface type that doesn't include that method. VS Code is saying that the method doesn't exist. Here's the relevant code
Factory Producer Class
/**
* Creates database or model factory.
*/
class FactoryProducer {
/**
* Creates a factory for the Model classes based on the given function argument.
*
* #param string $type The model class (e.g. 'asset', 'employee')
* #return ModelFactoryInterface The given model's factory.
*/
public static function getModelFactory(string $type) {
switch($type) {
case 'asset':
return new \Inc\Models\AssetModelFactory;
break;
case 'application':
//code here
break;
}
}
}
Concrete Factory Class AssetModelFactory
/**
* The factory for the Asset class.
*/
class AssetModelFactory implements ModelFactoryInterface {
/**
* Create an empty Asset class object.
*
* #return Asset
*/
function create(): Asset {
return new Asset();
}
/**
* Creates an Asset object instantiated with the given properties.
*
* #param array $props The properties for the class.
* #return void
*/
function createWithProps(array $props): Asset {
$asset = new Asset();
$keysToCheck = ['name', 'companyName', 'type', 'label', 'location', 'employees', 'key'];
if(\Inc\Base\Helpers::array_keys_exists($keysToCheck, $props)) {
$asset->setProperties($props['name'], $props['companyName'], $props['type'], $props['label'], $props['location'], $props['employees'], $props['key']);
return $asset;
}
else {
return new \WP_Error('incorrect_props', 'You did not include all of the necessary properties.');
}
}
}
The issue I'm having is with the second method, createWithProps(array $props), because the interface doesn't include this method:
/**
* The interface for model classes.
*/
interface ModelFactoryInterface {
/**
* Creates an object that extends AbstractModel
*
* #return AbstractModel
*/
public function create(): AbstractModel;
}
As you can see, the concrete class objects extend an abstract class. Here is the code that is giving the error:
$assetFactory = \Inc\Base\FactoryProducer::getModelFactory('asset');
$asset = $assetFactory->createWithProps($request);
I'm wondering if I've implemented the abstract factory class incorrectly, or if this is expected behavior from VS Code given that the returned concrete class from FactoryProducer is dynamic based on the parameter (e.g. I've passed 'asset' into the FactoryProducer::getModelFactory method which will, ultimately, return an instance of AssetModelFactory, but the official return type is ModelFactoryInterface).
Thank you in advance for any advice you can provide.
I was able to figure out what I did. I am used to programming languages like C# wherein I can strongly-type the variable prior to declaring it. I ended up refactoring the code so that the factories have methods that return a specific concrete object instead of using a switch statement:
class DBFactory implements DBFactoryInterface {
public static function createAsset(): AbstractDB {
return new \Inc\DB\AssetDB;
}
public static function createApplication(): AbstractDB {
return new \Inc\DB\ApplicationDB;
}
public static function createCompany(): AbstractDB {
return new \Inc\DB\CompanyDB;
}
}

Parent controller not resolving parameter "User $user", but "$id"

I have a parent controller "UserController", and a child controller "UserCartController". Unlike all other controllers, FOSRest refuses the following definition:
class UserController extends AbstractFOSRestController
{
// [...]
/**
* #param User $user
* #return View
*/
public function getUserAction(User $user): View
{
return new View([]);
}
}
This results in the following error:
Every parent controller must have `get{SINGULAR}Action(\$id)` method where {SINGULAR} is a singular form of associated object in [...]/config/../src/GDS/[BundleNameHere]/Resources/config/user_routes.yaml (which is being imported from "[...]/config/routes.yaml"). Make sure there is a loader supporting the "rest" type.
Once I change the head of the way as follows...
class UserController extends AbstractFOSRestController
{
// [...]
/**
* #param User $user
* #return View
*/
public function getUserAction($id): View
{
return new View([]);
}
}
The error no longer appears. What is the reason? In all other controllers, you can resolve model instances.
As a result, my routes don't look very elegant. :)
/api/users/{id}.{_format}
# vs.
/api/carts/{cart}.{_format}
Thanks in advance!

TYPO3 extbase: Get parent object in model object

I have got two (2) classes:
Person model class
<?php
class Person extends BaseDto
{
/**
* #var array|PostalAddress
*/
protected $postalAddresses = array();
/**
* #param array|PostalAddress $postalAddresses
*/
public function setPostalAddresses($postalAddresses)
{
$this->postalAddresses = $postalAddresses;
}
/**
* #return array|PostalAddress[]
*/
public function getPostalAddresses()
{
return $this->postalAddresses;
}
}
PostalAddress model class
<?php
class PostalAddress
{
/**
* #var string $privatePersonFirstName
*/
protected $privatePersonFirstName;
/**
* #var string $privatePersonName
*/
protected $privatePersonName;
/**
* #return string
*/
public function getPrivatePersonFirstName()
{
return $this->privatePersonFirstName;
}
/**
* #param string $privatePersonFirstName
*/
public function setPrivatePersonFirstName($privatePersonFirstName)
{
$this->privatePersonFirstName = $privatePersonFirstName;
}
/**
* #return string
*/
public function getPrivatePersonName()
{
return $this->privatePersonName;
}
/**
* #param string $privatePersonName
*/
public function setPrivatePersonName($privatePersonName)
{
$this->privatePersonName = $privatePersonName;
}
}
In the controller PostalAddressConroller I have got an action which creates the form to edit a single address.
I would like to make some fields editable only if certain conditions are met. Example: The organization fields on the address are only editable in case the person is of type private person and the address is of type employer.
To implement such a condition check, I would like to create a method on the PostalAddress model. But for this, It would require to have a reference back to the parent object inside the controller.
I would like to avoid to put all the logic inside the templates to keep the templates clean and easy to understand.
Is there support on extbase level for such back references?
In case I have to implement such a back reference myself: How do I prevent circular references in general (for example on object serialization)?
I would handle the problem differently. This is no controller job imho. This is definetly a template/view job. I'd use if conditions in the template to show the correct layout (field editable or not). Afterwards you have to make sure that nobody can just make the fields editable via developer tools for example.
That would be achievable by adding conditions in the backend logic, for example:
if($model->isAllowedProperty) { AddFieldToResultArrOrSimilar() }
As I understand the MVC pattern, the model should only carry data, no logic and no dependencies of any kind.
So to solve you problem I would use two different model classes, based on the same table, carying only those properites and validation metadata which applies to that particular model.
Imagine those two models below:
namespace Acme\MyPlugin\Domain\Model;
class PostalAddressPrivate
{
/**
* #var string $privatePersonFirstName
*/
protected $privatePersonFirstName;
/**
* #var string $privatePersonName
*/
protected $privatePersonName;
[...]
}
namespace Acme\MyPlugin\Domain\Model;
class PostalAddressCommercial
{
/**
* #var string $privatePersonFirstName
*/
protected $companyName;
[...]
}
Now you must tell the persistence layer, that those models goes to the same table. You do this in typoscript setup for that plugin.
plugin.tx_myplugin {
persistence {
classes {
Acme\MyPlugin\Domain\Model\PostalAddressPrivate {
mapping {
tableName = tx_myplugin_domain_model_postal_address
}
}
Acme\MyPlugin\Domain\Model\PostalAddressCommercial {
mapping {
tableName = tx_myplugin_domain_model_postal_address
}
}
}
}
Now you can transfer the logic into the controller and decide there which model to use. You can extend this simple case using a common Interface or abstract class, etc.
This "choose the right model" logic in the controller may be a little bit tricky at times. In general you will need to place some code dealing with the extbase "property mapper" inside the appropriate "initializeXxxAction" method.
At the begining I was inspired by this article in german (for an older version extbase!): https://jweiland.net/typo3/codebeispiele/extension-programmierung/extbase-dynamische-validierung.html
Hope google translate will give you some hints to solve upcomming problems.
On top you can assist the server based validation and processing by some frontend work. E.g. JavaScript tricks to enable or disable certain formular fields depending on the private/commercial status chosen.
You can as well tune the fluid templates to render/not render certain parts depending of the model variant used in controller.

Custom functions in Doctrine2 auto generated classes

Is there a way to extend classes auto-generated from database by Doctrine2 ?
Example: I have this User class generated by Doctrine.
<?php
namespace Entities;
/**
* User
*/
class User
{
/**
* #var integer
*/
private $id;
/**
* #var string
*/
private $firstName;
/**
* #var string
*/
private $lastName;
/**
* Get id
*
* #return integer
*/
public function getId()
{
return $this->id;
}
/**
* Set firstName
*
* #param string $firstName
*
* #return User
*/
public function setFirstName($firstName)
{
$this->firstName = $firstName;
return $this;
}
/**
* Get firstName
*
* #return string
*/
public function getFirstName()
{
return $this->firstName;
}
/**
* Set lastName
*
* #param string $lastName
*
* #return User
*/
public function setLastName($lastName)
{
$this->lastName = $lastName;
return $this;
}
/**
* Get lastName
*
* #return string
*/
public function getLastName()
{
return $this->lastName;
}
I would like to add this function :
public function getFullName()
{
return $this->getFirstName().' '.$this->getLastname();
}
Is there a cleaner way than adding it directly into this class?
I tried to create another class (Test) in libraries and extends it, then add it in autoload (which is working), but i get an error when I try to save object :
class Test extends Entities\User {
public function getFullName() {
return $this->getFirstName().' '.$this->getLastname();
}
}
Message: No mapping file found named 'Test.dcm.yml' for class 'Test'.
I'm using Doctrine2 in CodeIgniter3.
Thanks.
As explained in the Doctrine 2 FAQ:
The EntityGenerator is not a full fledged code-generator that solves all tasks. [...] The EntityGenerator is supposed to kick-start you, but not towards 100%.
In plain English this means you ask Doctrine to generate the Entity files only once. After that, you are on your own and do whatever changes you like (or it needs) to them.
Because an Entity is not just a container for some properties but it's where the entire action happens, this is how the flow should happen, Doctrine cannot write more code for you.
The only way to add functionality to the stub Entities generated by Doctrine is to complete the generated classes by writing the code that implements the functionality of each Entity according to its role in your Domain Model.
Regarding the other issue, on the Test class, the error message is self-explanatory: any class passed to the EntityManager for handling needs to be mapped.
Take a look at the help page about Inheritance Mapping. You can either map class User as a Mapped Superclass (it acts like a template for the derived classes and its instances are not persisted in the database) or you can use Single Table Inheritance to store the instances of all classes derived from User in a single table (useful when they have the same properties but different behaviour).
Or, in case you created class Test just because you were afraid to modify the code generated by Doctrine, put the behaviour you need in class User and drop class Test.
Seems you are having trouble while accessing the user entity class. You mentioned that test is a library class. Why not try to access the User entity class from a controller. If can do this then may be something is wrong with the configuration of test file. Besides, you need to map you doctrine entity class properly. You can have a look here to learn about doctrine mapping using yml: http://doctrine-orm.readthedocs.org/en/latest/reference/yaml-mapping.html
you can do this:
<?php
namespace Entities;
/**
* User
*/
class User extends Test
{
//... and extends Test
}
or
<?php
namespace Entities;
/**
* User
*/
class User
{
//...
public function getFullName() {
return $this->getFirstName().' '.$this->getLastname();
}
}
view more
Symfony 2 - Extending generated Entity class
http://www.theodo.fr/blog/2013/11/dynamic-mapping-in-doctrine-and-symfony-how-to-extend-entities/
http://doctrine-orm.readthedocs.org/en/latest/reference/inheritance-mapping.html
Annotation allows you to specify repository class to add more methods to entity class.
/**
* #ORM\Entity(repositoryClass="App\Entity\UserRepository")
*/
class User
{
}
class UserRepository extends EntityRepository
{
public function getFullName() {
return $this->getFirstName().' '.$this->getLastname();
}
}
// calling repository method
$entityManager->getRepository('User')->getFullName();
Here's a link [http://doctrine-orm.readthedocs.org/en/latest/reference/working-with-objects.html]
7.8.8. Custom Repositories

How to implement Repository Pattern in Codeigniter?

When I programmed in ASP.NET MVC, there was a neat pattern called Repository. I want to implment it in Codeigniter but I do not know how. Here is what I actually want:
$mock_repository = new MockRepository();
$mock_repository->add(new Item(‘title1′, ‘description1′, 1));
$mock_repository->add(new Item(‘title2′, ‘description2′, 2));
$mock_repository->add(new Item(‘title3′, ‘description3′, 1));
$controller = new Item_controller($mock_repository);
$items = $controller->get_items_by_user_id(1);
$this->_assert_equals(count($items), 2);
I am using TOAST for Unit Testing. So how do I instantiate a controller within a test? The test is of course, another controller itself.
From what I know, to create a Generic Repository Pattern like in C#, you need 2 things PHP 5.6 dosen't have:
Real Method Overloading.
Generic Interface or Generic Abstract Class in PHP.
Click here for more on Generic Repository Pattern in C#.
However you can still create pseudo method overloading in PHP with the help of magic method __call, and we can type little more code for the generic part of the pattern.
Note: Before creating this pattern in Codeigniter 3.0 you will need to create a table in the database, and create auto loader for folder application/libraries.
First we need to create Interface in application/libraries folder:
<?php
interface IRepository
{
public function getById($id);
public function select($columns);
public function delete($id);
}
Seconde we need to create Abstract Class implementing the Interface and extending the CI_Model to be able to use the Database librarie:
<?php
abstract class Base_repository extends CI_Model implements IRepository
{
/**
* This must be valid table name in the Database.
*
* #var string $table Name of the table.
*/
protected $table;
public function __construct()
{
parent::__construct();
}
/**
* Pseudo method overloading.
* It's called when method is not declared in the abstract class.
*
* #param string $name Name of the method
* #param mixed $arguments Arguments of the method
*/
public function __call($name, $arguments)
{
switch ($name)
{
case 'save':
if ($arguments[0]->id > 0)
{
$this->update($arguments[0]);
}
else
{
$this->insert($arguments[0]);
}
break;
}
}
/**
* Get row with id.
*
* #param integer $id
* #return mixed
*/
public function getById($id)
{
return $this->db->get_where($this->table, ['id' => $id])->row_array();
}
/**
* Select columns.
*
* #param array $columns
* #return mixed
*/
public function select($columns = ['*'])
{
$this->db->select($columns);
return $this->db->get($this->table)->result();
}
/**
* Insert data.
*
* #param object $item
* #return void
*/
private function insert($item)
{
unset($item->id);
$this->db->insert($this->table, $item);
}
/**
* Update data.
*
* #param object $item
* #return void
*/
private function update($item)
{
$this->db->where('id =', $item->id);
unset($item->id);
$this->db->update($this->table, $item);
}
/**
* Delete data.
*
* #param integer $id
* #return void
*/
public function delete($id)
{
$this->db->delete($this->table, ['id' => $id]);
}
}
Third test the repository. Make a new model in application/model, and extend Base_repository, set table name and overload save method, create entity for this model:
<?php
/**
* The entity class.
*/
class Test
{
public $id;
public $info;
}
class Test_model extends Base_repository
{
/**
* Tell what table we are using.
*/
public function __construct()
{
parent::__construct();
$this->table = 'test';
}
/**
* "Overload" save method and call it from the parent.
*
* #param test $item Make use of the Dependency Injection.
* #return void
*/
public function save(Test $item)
{
parent::save($item);
}
}
Try it in the controller. Load the model and try to get, insert, ect...
To create real models is the same procedure. If you need to add more methods that will be the same for every model add them in the abstract class if you need to create methods only for specific model add it only in this model.
I don't recommend Codeigniter freamwork. Here are some patterns for PHP CLICK!
You would have to completely hijack the system files to load a controller from another controller. It can't be done, methinks.
It can be done with HMVC.
$result = Modules::run('controller/get_items_by_user_id', $params);
$this->_assert_equals($result, $expected);

Categories