⬆️ ⬇️

How to connect Yii Framework and Doctrine 2 ORM?





I really like the Yii Framework. It is fast, convenient, flexible. I like how the ActiveRecord pattern is implemented in it. But there are cases when business logic, and, to be precise, domain logic, is very complex and is constantly growing and modifying. In such cases, it is more convenient to use the DataMapper pattern.



At the same time, I like Doctrine 2 ORM. This is probably the most powerful ORM for PHP, which has the widest functionality. Yes, perhaps it is “heavy” and slows down the application. But when starting a development, first of all, you should think about the architecture of the application, since “premature optimization is the root of all ills”

')

Thus, once I had the idea to link 2 of these interesting tools to me. How this was done is described below.



Install the required libraries



It was decided to link Doctrine and Yii by creating the corresponding DoctrineComponent component, which would provide access to the Doctrine functions.



First of all, in the protected folder of the framework, the vendor folder was created, where the Doctrine 2 ORM code was downloaded. You can install Doctrine using Composer or simply by downloading / sloping the source from the Doctrine project's GitHub.

Also, for ORM to work correctly, Doctrine Database Abstraction Layer and Doctrine Common will be needed (when installing Doctrine 2 ORM using Composer, these dependencies are automatically tightened).



In addition, I advise that in order to work with Doctrine 2 ORM via the console, install the Symfony component in the same folder as the vendor 2 (for working with Doctrine via the console) and Yaml (if you wish to describe entities on Yaml)



Thus, at this stage, the following project structure should be obtained:







Creating the DoctrineComponent Component



Now you can go directly to creating the DoctrineComponent component. Below I will give the entire component code, since it is rather small. This code should be located in the protected / components folder in the DoctrineComponent.php file.



DoctrineComponent.php file code
use Doctrine\ORM\EntityManager; use Doctrine\ORM\Configuration; use Doctrine\ORM\Mapping\Driver\AnnotationDriver; use Doctrine\Common\Annotations\AnnotationReader; use Doctrine\Common\Annotations\AnnotationRegistry; class DoctrineComponent extends CComponent { private $em = null; private $basePath; private $proxyPath; private $entityPath; private $driver; private $user; private $password; private $host; private $dbname; public function init() { $this->initDoctrine(); } public function initDoctrine() { Yii::setPathOfAlias('Doctrine', $this->getBasePath() . '/vendor/Doctrine'); $cache = new Doctrine\Common\Cache\FilesystemCache($this->getBasePath() . '/cache'); $config = new Configuration(); $config->setMetadataCacheImpl($cache); $driverImpl = new AnnotationDriver(new AnnotationReader(), $this->getEntityPath()); AnnotationRegistry::registerAutoloadNamespace('Doctrine\ORM\Mapping', $this->getBasePath() . '/vendor'); $config->setMetadataDriverImpl($driverImpl); $config->setQueryCacheImpl($cache); $config->setProxyDir($this->getProxyPath()); $config->setProxyNamespace('Proxies'); $config->setAutoGenerateProxyClasses(true); $connectionOptions = array( 'driver' => $this->getDriver(), 'user' => $this->getUser(), 'password' => $this->getPassword(), 'host' => $this->getHost(), 'dbname' => $this->getDbname() ); $this->em = EntityManager::create($connectionOptions, $config); } public function setBasePath($basePath) { $this->basePath = $basePath; } public function getBasePath() { return $this->basePath; } public function setEntityPath($entityPath) { $this->entityPath = $entityPath; } public function getEntityPath() { return $this->entityPath; } public function setProxyPath($proxyPath) { $this->proxyPath = $proxyPath; } public function getProxyPath() { return $this->proxyPath; } public function setDbname($dbname) { $this->dbname = $dbname; } public function getDbname() { return $this->dbname; } public function setDriver($driver) { $this->driver = $driver; } public function getDriver() { return $this->driver; } public function setHost($host) { $this->host = $host; } public function getHost() { return $this->host; } public function setPassword($password) { $this->password = $password; } public function getPassword() { return $this->password; } public function setUser($user) { $this->user = $user; } public function getUser() { return $this->user; } /** * @return EntityManager */ public function getEntityManager() { return $this->em; } } 




The main part of the component is enclosed in the initDoctrine method. Let's sort out the code in more detail.



 $cache = new Doctrine\Common\Cache\FilesystemCache($this->getBasePath() . '/cache'); $config = new Configuration(); $config->setMetadataCacheImpl($cache); 




With this code, we set the method for caching the metadata of the entities from Doctrine. In an amicable way, the type of caching (in this case, FilesystemCache ) should be better made into the parameters of the component, which we could change when configuring the component.



 $driverImpl = new AnnotationDriver(new AnnotationReader(), $this->getEntityPath()); AnnotationRegistry::registerAutoloadNamespace('Doctrine\ORM\Mapping', $this->getBasePath() . '/vendor'); $config->setMetadataDriverImpl($driverImpl); 




Using the code above, a driver is installed to read the entity metadata.



  $config->setQueryCacheImpl($cache); $config->setProxyDir($this->getProxyPath()); $config->setProxyNamespace('Proxies'); $config->setAutoGenerateProxyClasses(true); 




In the code above, we set the caching method for requests (first line), the remaining lines are the setting of the Proxy for Doctrine (path, namespace, installation of automatic generation of Proxy classes)



 $connectionOptions = array( 'driver' => $this->getDriver(), 'user' => $this->getUser(), 'password' => $this->getPassword(), 'host' => $this->getHost(), 'dbname' => $this->getDbname() ); $this->em = EntityManager::create($connectionOptions, $config); 




The code above defines the options for connecting to the database. These parameters are set when the component is connected (it will be shown later how to connect the component).

And at the end EntityManager is created with certain earlier $connectionOptions and $config , with which you can work with our entities.



How to connect DoctrineComponent to the project?



Let's move on to connecting the DoctrineComponent to the project.

It is quite simple to do this - you just need to make changes to the project configuration file (usually it is main.php)

 return array( 'components' => array( 'doctrine'=>array( 'class' => 'DoctrineComponent', 'basePath' => __DIR__ . '/../', 'proxyPath' => __DIR__ . '/../proxies', 'entityPath' => array( __DIR__ . '/../entities' ), 'driver' => 'pdo_mysql', 'user' => 'dbuser', 'password' => 'dbpassword', 'host' => 'localhost', 'dbname' => 'somedb' ), // ... ); 




Now our component will be available through Yii::app()->doctrine , and we can get the EntityManager through Yii::app()->doctrine->getEntityManager()



But with this use of the component, a problem arises in the method hints for the EntityManager object. For this, the following solution was invented:

 lass MainController extends Controller { private $entityManager = null; /** * @return Doctrine\ORM\EntityManager */ public function getEntityManager() { if(is_null($this->entityManager)){ $this->entityManager = Yii::app()->doctrine->getEntityManager(); } return $this->entityManager; } // ... } 




Each controller is now inherited from MainController and thus, in each controller, you can call the $this->getEntityManager() method to get an entity manager, and the IDE will now work with method hints for the EntityManager , which is undoubtedly a plus.



Doctrine console configuration



It is very convenient to work with Doctrine through its console. But for this you need to write code to run it. This code is shown below. I put the file to start the console in the protected / commands folder. It would also be very good to implement the doctrine command to start the console even more simply, but I haven’t done it yet.



Sample doctrine.php file for working with the Doctrine console.

File code doctrine.php
 // change the following paths if necessary $yii = __DIR__ .'path/to/yii.php'; $config = __DIR__ . 'path/to/config/console.php'; require_once($yii); Yii::createWebApplication($config); Yii::setPathOfAlias('Symfony', Yii::getPathOfAlias('application.vendor.Symfony')); $em = Yii::app()->doctrine->getEntityManager(); $helperSet = new \Symfony\Component\Console\Helper\HelperSet(array( 'db' => new \Doctrine\DBAL\Tools\Console\Helper\ConnectionHelper($em->getConnection()), 'em' => new \Doctrine\ORM\Tools\Console\Helper\EntityManagerHelper($em) )); \Doctrine\ORM\Tools\Console\ConsoleRunner::run($helperSet); 






To start the Doctrine console, simply go to the commands folder and execute php doctrine.php .



Validating models and using models in a GridView widget.



Those who worked with Doctrine 2 ORM know that in fact there are no models in their generally accepted concept (with validation methods, data acquisition from databases, business logic included, etc.), and this functionality is actually divided into 2 parts - Entity and Repository . Entity typically includes business logic, and in Repository , methods for retrieving data from a database using DBAL Doctrine (or entity manager, or other methods).



Validation of models


Thus, in my opinion, it would be logical to include data validation in the class of a specific entity.

Consider the User entity as an example.

In order not to reinvent the wheel, it was decided that it would be nice to use the already built-in validation of models from Yii, and specifically from the CModel. class CModel.



To do this, you can simply inherit the User entity from the CModel class. An example of such an entity with the described validation rules is below:



User Entity Code
 use Doctrine\ORM\Mapping as ORM; /** * User * * @ORM\Table(name="user") * @ORM\Entity(repositoryClass="UserRepository") * @ORM\HasLifecycleCallbacks */ class User extends CModel { /** * @var integer * * @ORM\Column(name="id", type="integer", nullable=false) * @ORM\Id * @ORM\GeneratedValue(strategy="IDENTITY") */ private $id; /** * @var string * * @ORM\Column(name="name", type="string", length=255, nullable=false) */ private $name; /** * @var string * * @ORM\Column(name="password", type="string", length=255, nullable=false) */ private $password; /** * @var string * * @ORM\Column(name="email", type="string", length=255, nullable=false) */ private $email; /** * @var string * * @ORM\Column(name="role", type="string", length=255, nullable=false) */ private $role; /** * @var \DateTime * * @ORM\Column(name="created", type="datetime", nullable=false) */ private $created; /** * @var \DateTime * * @ORM\Column(name="modified", type="datetime", nullable=false) */ private $modified; public function rules(){ return array( array('name, password', 'required'), // ... ); } public function attributeNames() { return array( 'id'=>'id', 'name'=>'name', 'email'=>'email', 'created'=>'created', 'updated'=>'updated' ); } public function attributeLabels() { return array( 'description' => 'Description', 'createdString' => 'Creation Date' ); } // ... } 




Now I will give an example of how to work with this validation (an example of creating a new user below):

  /** * Creates a new model. * If creation is successful, the browser will be redirected to the 'view' page. */ public function actionCreate() { $user = new User(); $userData = $this->getRequest()->get('User'); $course->setAttributes($userData); if(!is_null($userData) && $user->validate()) { $user->setName($userData['name']); // ...      $this->getEntityManager()->persist($user); $this->getEntityManager()->flush(); $this->redirect(array('view','id'=>$user->getId())); } $this->render('create',array( 'model'=>$user, )); } 






Using models in a GridView widget


One of the main charms of Yii are, in my opinion, widgets, and especially the various Grid, which go to Yii from the box.

But the only caveat is that they work with ActiveRecord (I mean the GridView widget). And personally, I would like to get them to work with Doctrine and entities. For this you can use the Repository .



When using the GridView there are 2 bottlenecks - the dataProvider and filter properties. And here I sing odes to Yii developers - in order for GridView work with some data other than ActiveRecord , it is enough that the object transferred to the GridView as a dataProvider correctly implements the IDataProvider interface (this interface should be implemented in our UserRepository ), and the object passed to filter must inherit from CModel (our User entity is already well suited for this).



I will not give the entire implementation of the UserRepository , I will outline only the general scheme.

BaseRepository class code
 use Doctrine\ORM\EntityRepository; abstract class BaseRepository extends EntityRepository implements IDataProvider { protected $_id; private $_data; private $_keys; private $_totalItemCount; private $_sort; private $_pagination; public $modelClass; public $model; public $keyAttribute; private $_criteria; private $_countCriteria; public $data; abstract protected function fetchData(); abstract protected function calculateTotalItemCount(); public function getId(){ //... } public function getPagination($className='CPagination'){ //... } public function setPagination($value){ //... } public function setSort($value){ //... } public function getData($refresh=false){ //... } public function setData($value){ //... } public function getKeys($refresh=false){ //... } public function setKeys($value){ //... } public function getItemCount($refresh=false){ //... } public function getTotalItemCount($refresh=false){ //... } public function setTotalItemCount($value){ //... } public function getCriteria(){ //... } public function setCriteria($value){ //... } public function getCountCriteria(){ //... } public function setCountCriteria($value){ //... } public function getSort($className='CSort'){ //... } protected function fetchKeys(){ //... } private function _getSort($className){ //... } } 






The above is an example implementation of a base repository. In fact, the implementation of many methods can be found in the Yii class CActiveDataProvider which implements the IDataProvider interface. In the UserRepository we only have to define 2 methods (sample code below):



 <?php class UserRepository extends BaseRepository { protected $_id = 'UserRepository'; /** * Fetches the data from the persistent data storage. * @return array list of data items */ protected function fetchData() { //... } /** * Calculates the total number of data items. * @return integer the total number of data items. */ protected function calculateTotalItemCount() { //... } } 




Summary



Above, I cited one of their ways of how to work in a bunch of Yii + Doctrine 2 ORM. Many may say that because of Doctrine 2 ORM, Yii will lose its advantages, but we should not forget that Doctrine has a huge amount of tools for optimization and caching, and no one forbids rewriting too slow or intense queries on Plain SQL.

But in such a bundle we win in the architectural solution and in my opinion, the code becomes clearer from this.



I would be very grateful if, in the comments, you shared your options on how to implement the DataMapper pattern, some other ORMs in Yii, about your ways of solving the growth of business logic in ActiveRecord models in Yii, about subject-oriented programming using Yii.



Thanks for attention.

Source: https://habr.com/ru/post/208182/



All Articles