Symfony / Doctrine: Restful API design and design pattern about repository injection - symfony

I'm new to Symfony and these questions were brought about in the recent course of learning.
Take a store as an example, I'll create two entities, Product and Category, which have a bidirectional Many-to-One relationship.
class Product
{
private $id;
private $name;
/**
* #ORM\ManyToOne(targetEntity="App\Entity\Category", inversedBy="products")
* #ORM\JoinColumn(name="category_id", referencedColumnName="id", nullable=false)
*/
private $category;
}
class Category
{
private $id;
private $name;
/**
* #ORM\OneToMany(targetEntity="App\Entity\Product", mappedBy="category")
*/
private $products;
}
So my 1st question is:
If I want to get all the products in a particular category, should the URL be
/categories?categoryId=1&limit=20&orderBy=name
(I know it's a bit silly, but should a Category record contains all the Product information?)
or
/products?categoryId=1&limit=20&orderBy=name
For the latter one, here comes the 2nd question:
I injected the ProductRepository to the ProductController
class ProductController extends Controller
{
private $productRepository;
public function __construct(ProductRepository $productRepository)
{
$this->productRepository = $productRepository;
}
...
}
In order to get all the product in a category, I wrote a method like this:
public function findByCategory(Category $category): array
{
return $this->createQueryBuilder('p')
->andWhere('p.category = :category')
->setParameter('category', $category)
->orderBy('p.name', 'ASC')
->setMaxResults(20)
->getQuery()
->getResult()
;
}
So, in the ProductController, how should I get the Category object from the query string 'categoryId' in the URL? Should I inject CategoryRepository as well or should I simply inject an entity manager object?

Marco Pivetta aka Ocramius (one of the main developers of Doctrine) said:
Avoid bi-directional associations
Bi-directional associations are overhead
Code only what you need for your domain logic to work
Hack complex DQL queries instead of making them simpler with bidirectionality
So maybe
you don't need bi-directional association here.
For your first question, the second solution is better in my opinion:
/products?categoryId=1&limit=20&orderBy=name
For your second question yes, you should inject the CategoryRepository if you want to access Category object, avoid accessing the whole entityManager in your controller even if it is possible.
You should inject services in your controllers. Your services should expose public methods to perform custom CRUD access to the DB through data mappers. Note that a repository is not a data mapper but it
mediates between the domain and data mapping layers, acting like an in-memory domain object collection.
P of EAA Catalog - Martin Fowler
Repositories are services in fact so that's fine to inject them into the controller.
Some people defend the position that repositories should not contain CREATE, UPDATE or DELETE but only READ. They say that these operations make collections (which are accessible through repositories) inconsistent.
This post can help too: How should a model be structured in MVC?

To me, the problem here is a very clear confusion between what the ORM does in Symfony and Database Design, Modelling and Querying.
In the database (using PhpMyAdmin), you'll notice that on the product table there is a column called category (or category_id). Keep it simple, to get products belonging to a category, all you need is the category_id. Take that knowledge to Symfony, you DO NOT need the Category Object, please just use the category ID that you got from the request. Also, just use the EntityManager in the controller, don't complicate stuff, especially since it seems you're just starting out.
use Symfony\Component\HttpFoundation\Request;
class ProductController extends Controller
{
public function get_product_from_categoryAction(Request $request)
{
$category_id = (int) $request->get('category');
$limit = (int) $request->get('limit');
$orderBy = strip_tags($request->get('orderBy'));
$em = $this->getDoctrine()->getManager();
$products = $em
->getRepository('AppBundle:Products')
->queryProductsByCategoryId($category_id, $limit, $orderBy);
}
...
}
And the repo
public function queryProductsByCategoryId(int $category_id, int $limit = 10, string $orderBy = 'name')
{
return $this->createQueryBuilder('p')
->andWhere('p.category = :category')
->setParameter('category', $category_id)
->orderBy('p.name', 'ASC')
->setMaxResults($limit)
->getQuery()
->getResult()
;
}
Keep things simple, then when you got to be more advanced, try the more fancy stuff if you so please.

Related

How to store historical data with symfony and doctrine?

I want to store store historical data with symfony2 and doctrine2. For example i am having 2 entities:
class Shop
{
private $id;
private $url;
private $version;
}
and the second entity:
class Version
{
private $id;
private $software;
private $version;
}
The Version entity stores specific shop-versions, for example Magento 1.2 or OXID eShop 4.7 - so a entry for a version-entity should be reusable.
Every time the version for a Shop is changed, i want to store this change to have a historical view for the version-changes.
How can i do that with symfony2 and doctrine2? I have tried many-to-many mappings, but i cant figure out the right way using the correct mapping.
Thanks for your help!
There's a few things you have to set properly in order for this to work.
First, you need to tell Doctrine that $versions is related to Version:
class Shop
{
private $id;
private $url;
/**
* #ORM\ManyToMany(targetEntity="Version", cascade={"persist"})
* #ORM\JoinTable(name="shop_version",
* joinColumns={#ORM\JoinColumn(name="shop_id", referencedColumnName="id")},
* inverseJoinColumns={#ORM\JoinColumn(name="version_id", referencedColumnName="id")}
* )
*/
private $versions;
}
Since it's a ManyToMany relationship (documentation), $versions will be treated like an ArrayCollection by Symfony. Thus, you need to create methods to handle it accordingly.
Constructor
public function __construct()
{
$this->versions = new ArrayCollection();
}
Getter
public function getVersions()
{
return $this->versions;
}
Adder
public function addVersion(Version $version)
{
$this->versions[] = $version;
}
Remover
public function removeVersion(Version $version)
{
$this->versions->removeElement($version);
}
That's it. Don't forget to add the use statment for ArrayCollection!
use Doctrine\Common\Collections\ArrayCollection;
In your case instead of reinventing the wheel i would recommend Doctrine2 extension: EntityAudit that allows full versioning of entities and their associations. Usage:
$auditReader = $this->container->get("simplethings_entityaudit.reader");
// find entity state at a particular revision
$articleAudit = $auditReader->find('SimpleThings\EntityAudit\Tests\ArticleAudit', $id = 1, $rev = 10);
// find Revision History of an audited entity
$revisions = $auditReader->findRevisions('SimpleThings\EntityAudit\Tests\ArticleAudit', $id = 1);
// find Changed Entities at a specific revision
$changedEntities = $auditReader->findEntitiesChangedAtRevision( 10 );
and more on: https://github.com/simplethings/EntityAudit
Another available package for entity versioning is https://github.com/madmis/ActivityLogBundle. This package includes a revision control system that saves each state of your desired entities and properties.
To enable logging, add the following annotation to your entity class
#Gedmo\Loggable(logEntryClass="ActivityLogBundle\Entity\LogEntry")
Make sure to import the annotation
use Gedmo\Mapping\Annotation as Gedmo;
Add the following annotations to the properties where you want to log changes of
#Gedmo\Versioned
The package offers methods to easily retrieve logentries for an entity
public function getLogEntriesQuery($entity)
This will return log entries with the following methods
$logEntry->getVersion() //returns entities revision version
$logEntry->getOldData() //returns data state before updating
$logEntry->getData() //returns data state after updating
$logEntry->getLoggedAt() //returns when de log was created
In order to retrieve logEntries for a given timeframe you can extend the querybuilder that's returned from the following method, which is also available in the LogEntryRepository:
public function getLogEntriesQueryBuilder($entity)

Link 1 entity with many others kind of entities

Let say I have a Company for which I manage Employees, Cars, Contracts, Buildings, Sites, Products, etc. As you can guess, these are quite independant things, so no inheritance is possible.
For each of these elements (i.e. Entities), I want to be able to attach one or several Documents (click on a button, form opens, select one/several Document or upload a new one).
Linking Document to one kind of entity is not a problem, my problem is that there are many kinds of entities. How should I manage that? I have 2 ideas which have their own problems...:
Create a ManyToMany relationship between Document and Employee, another between Document and Car, etc.
Problem: I have to duplicate the Controller code to attach Document, duplicate the forms, etc.
Create a single join table containing the Document's ID, the related entity's ID and the related entity's class name.
Problem: it doesn't look really clean to me, I didn't really dig in this way but I feel I'll have a lot of "entity mapping" problems.
Any suggestion?
[EDIT]
In fact I have to do the same for Event as well: I need to link some Events to some Employees and/or to some Cars, etc. And in my real case, I have more than 10 Entities to be linked to Event and/or Document, which means duplicating more tha 20 times the code if I go with the solution 1!
Assuming you're using Doctrine ORM, i think you're searching for the Mapped Superclasses inheritance.
The docs are better than words :
http://docs.doctrine-project.org/projects/doctrine-orm/en/latest/reference/inheritance-mapping.html#mapped-superclasses
So finally I managed to solve my problem, following #Rpg600 idea about Mapped Superclasses.
This is probably not the best and cleanest solution ever, I'm not really proud of it but it does the job and it is still better than my first ideas.
I create a BaseEntity which is my a mapped superclass (Employee, Car, etc. Entities have to extend this Class):
/**
* BaseEntity
* #ORM\MappedSuperclass
*/
class BaseEntity {
/**
* #ORM\OneToOne(targetEntity="MyProject\MediaBundle\Entity\Folder")
*/
private $folder;
/**
* Set folder
* #param \Webobs\MediaBundle\Entity\Folder $folder
* #return BaseEntity
*/
public function setFolder(\Webobs\MediaBundle\Entity\Folder $folder = null){
$this->folder = $folder;
return $this;
}
/**
* Get folder
* #return \Webobs\MediaBundle\Entity\Folder
*/
public function getFolder(){
return $this->folder;
}
}
As it is not possible to have a Many-to-Many relationship in a superclass, I use a Folder which will contain one or several Document. This is the dirty part of the solution ; the folder table basically contain only one field which is the id...
class Folder
{
private $id;
/**
* Note : Proprietary side
* #ORM\ManyToMany(targetEntity="MyProject\MediaBundle\Entity\Document", inversedBy="folders", cascade={"persist"})
* #ORM\JoinTable(name="document_in_folder")
*/
private $documents;
// setters and getters
Then I create a helper class (which is declared as a service) to manage the link between any Entity and the Document:
class DocumentHelper extends Controller
{
protected $container;
/** ************************************************************************
* Constructor
* #param type $container
**************************************************************************/
public function __construct($container = null)
{
$this->container = $container;
}
/** ************************************************************************
* Attach Document(s) to an $entity according to the information given in the
* form.
* #param Entity $entity
* #param string $redirectRouteName Name of the route for the redirection after successfull atachment
* #param string $redirectParameters Parameters for the redirect route
* #return Response
**************************************************************************/
public function attachToEntity($entity, $redirectRouteName, $redirectParameters)
{
$folder = $entity->getFolder();
if($folder == NULL){
$folder = new Folder();
$entity->setFolder($folder);
}
$form = $this->createForm(new FolderType(), $folder);
// ------------- Request Management ------------------------------------
$request = $this->get('request');
if ($request->getMethod() == 'POST') {
$form->bind($request); // Link Request and Form
if ($form->isValid()) {
$em = $this->getDoctrine()->getManager();
$em->persist($folder);
$em->persist($entity);
$em->flush();
return $this->redirect($this->generateUrl($redirectRouteName, $redirectParameters));
}
}
return $this->render('MyProjectMediaBundle:Folder:addDocument.html.twig', array(
'form' => $form->createView(),
'entity' => $entity,
));
}
Doing that way, I just have to add one small action in each relevant controller, let say EmployeeController.php:
public function addDocumentAction(Employee $employee)
{
$redirectRouteName = 'MyProjectCore_Employee_see';
$redirectParameters = array('employee_id' => $employee->getId());
return $this->get('myprojectmedia.documenthelper')->attachToEntity($employee,$redirectRouteName,$redirectParameters);
}
Same principle for the display, in the helper I have the common function which I call in my already-existing seeAction() and in the TWIG file I import the common "Document list" display.
That's all folks!
I hope this can help :)

symfony2 create doctrine collection from query

Not sure if this is possible or not but im looking to create a doctrine collection from a query. The idea is to populate the collection with some pre-set values so i can update the database think of it like an import/generate users from an old system into a new one. Im struggling with the repository bit.
Entity
// Portal\UserBundle\Entity\User.php
namespace Portal\UserBundle\Entity;
use Doctrine\ORM\Mapping AS ORM;
/**
* #ORM\Entity
*/
class User
{
/**
* #ORM\Id
* #ORM\Column(type="integer")
*/
private $id;
/**
* #ORM\Column(type="string", length=255, nullable=false)
*/
private $fistname;
/**
* Get id
*
* #return integer
*/
public function getId()
{
return $this->id;
}
// etc...
}
Repository
namespace Portal\UserBundle\Entity\Repository;
use Doctrine\ORM\EntityRepository;
class UserRepository extends EntityRepository
{
public function getGenerateNewUsers()
{
// acts as an import from an old user table
$sql = " SELECT firstname, surname, other FROM old_user_table ";
$userCollection = .... not sure how I link query?
return $userCollection;
}
}
Calling it inside the controller
With the above I intend to be able to fetch the newly generated users loop over them and have access to my entity methods objects etc.
class SetupController extends Controller
{
public function indexAction(){
$repository = this->getDoctrine()->getRepository('UserBundle:User');
$newUsers = $repository->getGenerateUsers();
// I can now have access to the users with something like
foreach($newUsers as $user){
$user->setFirstName('testing');
$user->save();
}
}
}
It's usually the case with imports like this that your legacy table doesn't directly map to your new one (in terms of field names, constraints, etc), and may not even be in the same DBMS, so really the best option is a slightly manual approach. Execute your SQL query against your legacy database in your favourite old-fashioned way to get your users as simple arrays, then loop through them and create entities:
//Set up an empty collection
$collection = new ArrayCollection();
/*Assuming PDO where you have set up and executed a PDO statement $pdoStatement,
but mysql_fetch_assoc or whatever is appropriate for your DBMS would work equally
well. $oldUser should just be a plain associative array*/
while($oldUser = $pdoStatement->fetch(PDO::FETCH_ASSOC)){
//Initialise an empty User entity
$newUser = new User();
//Set the properties on the entity using the values from your array
$newUser->setFirstName($oldUser['firstname']);
//etc
//Add your user to the collection
$collection->add($newUser);
}
return $collection
I notice you're thinking of calling save() on your User objects in your controller, but it doesn't generally work that way in Doctrine as your entities will be plain objects which don't inherit from anything and don't have any special methods. The way to save the entity to your new database is to grab the entity manager and call its persist method.
In your controller:
$entityManager = $this->get('Doctrine')->getManager();
foreach($users as $user){
//Manipulate the user here if you need to
//Then persist it
$entityManager->persist($user);
}
As an aside - if you wanted to get a collection of entities by executing a query against your new database that's a slightly different problem to which there's a much more elegant solution. Doctrine Query Language allows you to query your database in a SQL-like way while using the language of your objects. With DQL, the results of your queries will by default be hydrated into Doctrine entites.
Hogan mentions DQL. Here is what that would look like, but you'd have to make sure your old database was wired up. The result is a collection of entities, off which you could use method calls to store part or all of the data as you see fit.
namespace Portal\UserBundle\Entity\Repository;
use Doctrine\ORM\EntityRepository;
class UserRepository extends EntityRepository
{
public function getGenerateNewUsers()
{
$qb = $this->getEntityManager()
->getRepository('Bundle:Old_User')->createQueryBuilder('o');
$query = $qb->getQuery();
$results = $query->getResult();
return $results;
}
}

how displaying two ManyToMany entities Symfony2

I'm under SF2.0.15 with Doctrine2 and I have two entities.
-Expedition
- Step
To explain, one expedition can have several steps and one step can belong to several expeditions. In addition, one expedition belongs to his founder (named "owner" and stored in the User entity). So, I have chosen to make a ManyToMany joining between Expedition and Steps tables. In your opinion, is it a good choice or a wrong choice ?
I want to create a method which select all the steps which belong to one expedition (I have the expedition's Id which is contained in $id_exp). So, I have read lots of topics in the Internet but it always fail and I want to know why...
The entity Expedition.php
/**
* #ORM\ManyToOne(targetEntity="Easymuth\UserBundle\Entity\User")
* #ORM\JoinColumn(nullable=false)
*/
private $owner;
/**
* #ORM\ManyToMany(targetEntity="Easymuth\ExpeditionBundle\Entity\Step", cascade={"persist"})
*/
private $steps;
/**
* Add steps
*
* #param Easymuth\ExpeditionBundle\Entity\Step $steps
*/
public function addStep(\Easymuth\ExpeditionBundle\Entity\Step $step)
{
$this->steps[] = $step;
}
public function __construct()
{
$this->steps = new \Doctrine\Common\Collections\ArrayCollection();
}
/**
* Get steps
*
* #return Doctrine\Common\Collections\Collection
*/
public function getSteps()
{
return $this->steps;
}
ExpeditionRepository.php :
namespace Easymuth\ExpeditionBundle\Entity;
use Doctrine\ORM\EntityRepository;
class ExpeditionRepository extends EntityRepository
{
public function getStepsFromExpedition($id_exp) {
$qb = $this->createQueryBuilder('e')
->leftJoin('e.steps', 's')
->addSelect('s')
->where('e.id = :id')
->setParameter('id', $id_exp);
return $qb->getQuery()->getResult();
}
}
And finally in my controller, I have :
namespace Easymuth\ExpeditionBundle\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Easymuth\ExpeditionBundle\Entity\Expedition;
class MainController extends Controller
{
public function stepsAction($id_exp) {
$expedition = $this->getDoctrine()
->getEntityManager()
->getRepository('EasymuthExpeditionBundle:Expedition')
->getStepsFromExpedition($id_exp);
print_r($expedition->getSteps()); // it displays a very long contents........
//return $this->render('EasymuthExpeditionBundle:Main:steps.html.twig'));
}
}
The displayed error on the print_r (or var_dump) is :
Fatal error: Call to a member function getSteps() on a non-object in /Applications/MAMP/htdocs/easymuth/src/Easymuth/ExpeditionBundle/Controller/MainController.php
Thank you very much for your help !
It is a good choice, you have to use ManyToMany associations for this design, good point !
But be careful, if you want to add information in your association (like order for example, can be useful for step in expedition), you have to create a new entity.
Check here for more info.
Then, the problem is in your controller. (You don't need additional function in your repository)
If you want to get all the steps from one expedition, just do in your controller :
//find your expedition
$expedition = $this->getDoctrine()
->getEntityManager()
->getRepository('EasymuthExpeditionBundle:Expedition')
->find($id_exp);
//then get the steps from this expedition
$steps = $expedition->getSteps();
You have to be sure that the expedition with $id_exp does exist or it will throw an error when you want to use your $expedition variable (because it is set at null)
You can check existence this way :
if(!$expedition) {
//do some stuff like throwing exception
}

Symfony2 / Doctrine - Modifying all queries

Is it possible to run all doctrine queries through a walker of some sort so that I can modify the query based on the current user's credentials? Ideally, I wouldn't have to explicitly call a setHint for a custom walker on every query, as that would restrict my ability to pass the current SecurityContext into the walker.
Also, I'd prefer not to use a Doctrine Filter, as I can't modify join conditions with filters, and I'd be forced to use an "IN" clause, which would severely affect performance
Currently, I'm using a service that modifies the QueryBuilder based on a user's credentials, but this becomes tedious, as I need to call the service every time I create a new QueryBuilder, and is even more of a pain when Repositories come into play (as I'd need to inject the service into every repository that needs to modify the query.
Hopefully I've explained this clearly enough. Appreciate any feedback!
I think I have solved my own issue. If someone else has a more elegant way of doing achieving these results, feel free to explain. In order to modify all of my queries, I have created a custom EntityManager and custom EntityRepository.
In my custom EntityManager, I have overwritten 2 methods. create() and getRepository()
public static function create($conn, Configuration $config, EventManager $eventManager = null)
{
if ( ! $config->getMetadataDriverImpl()) {
throw ORMException::missingMappingDriverImpl();
}
switch (true) {
case (is_array($conn)):
$conn = \Doctrine\DBAL\DriverManager::getConnection(
$conn, $config, ($eventManager ?: new EventManager())
);
break;
case ($conn instanceof Connection):
if ($eventManager !== null && $conn->getEventManager() !== $eventManager) {
throw ORMException::mismatchedEventManager();
}
break;
default:
throw new \InvalidArgumentException("Invalid argument: " . $conn);
}
return new MyCustomEntityManager($conn, $config, $conn->getEventManager());
}
The only thing that is changed in this method is that I am returning my own EntityManger(MyCustomEntityManager). Then, I overlaid the getRepository method as follows:
public function getRepository($entityName)
{
$entityName = ltrim($entityName, '\\');
if (isset($this->repositories[$entityName])) {
return $this->repositories[$entityName];
}
$metadata = $this->getClassMetadata($entityName);
$repositoryClassName = $metadata->customRepositoryClassName;
if ($repositoryClassName === null) {
$repositoryClassName = "Acme\DemoBundle\Doctrine\ORM\MyCustomEntityRepository";
}
$repository = new $repositoryClassName($this, $metadata);
$this->repositories[$entityName] = $repository;
return $repository;
}
Here, I have only modified one line as well. Instead of relying on the DBAL Configuration to retreive the default $repositoryClassName, I have specified my own default repository Acme\DemoBundle\Doctrine\ORM\MyCustomEntityRepository.
Once you have created your own custom EntityRepository, the sky is the limit. You can inject services into the repository(I currently use JMS Di annotations, described below), or perform custom actions against a QueryBuilder in the createQueryBuilder method, like so:
use JMS\DiExtraBundle\Annotation as DI;
class MyCustomEntityRepository extends EntityRepository
{
private $myService;
public function createQueryBuilder($alias)
{
$queryBuilder = parent::createQueryBuilder($alias);
/** INSERT CUSTOM CODE HERE **/
return $queryBuilder;
}
/**
* #DI\InjectParams({
* "myService" = #DI\Inject("my_service_id")
* })
*/
public function setMyService(MyServiceInterface $myService)
{
$this->myService = $myService;
}
}
Once you have created your own EntityRepository, you should have all of your repositories that need this custom functionality extend MyCustomEntityRepository. You could even take it a step further and create your own QueryBuilder to further extend this.
You can write a custom AST Walker and setup your application to use this walker for all queries with defaultQueryHint (Doctrine 2.5 new feature) configuration option:
<?php
/** #var \Doctrine\ORM\EntityManager $em */
$em->getConfiguration()->setDefaultQueryHint(
Query::HINT_CUSTOM_TREE_WALKERS,
['YourWalkerFQClassName']
)

Resources