I have a request to develop 3 forms to collect information from users. They share some of the information like: name, address, date, amount and some fields are different, each form will have some attachments (3 or 2) which represent different types of documents. More than that on the 'amount ' field there are different validations to make, depending on which form is applied.
How should I implement this? After some research the Doctrine Single Table Inheritance seems to be the answer:
Official documentation: http://docs.doctrine-project.org/en/latest/reference/inheritance-mapping.html
Useful blog: https://blog.liip.ch/archive/2012/03/27/table-inheritance-with-doctrine.html
Because the objects (corresponding to the forms) to be saved in the database have a lot in common they can all be kept in just one table. The table needs to allow NULL for the fields which are not common for all forms.
In Symfony I will create a base entity class which will be extended by child entities (one child for each form). The base entity can be "abstract' as it is not supposed to be directly used (the child entity classes will be used just as regular entities ).
Because I want to have different validation on the 'amount' field (and I want to keep them in entity) I will move the 'amount' from the base class to child classes.
Also in the table there will be a column which will keep the type of the record (object typeA from formA, object typeB from formB, etc ). The equivalent of this column in Doctrine language is DiscriminatorColumn.
@ORM\DiscriminatorColumn: indicates which column will be used as discriminator (i.e. to store the type of item). You don’t have to define this column in the entity, it will be automagically created by Doctrine.
Exaple:
Base entity class:
<?php
namespace Bundle\Entity;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Validator\Constraints as Assert;
/**
* Base entity class
*
* @ORM\Table(name="table")
* @ORM\Entity
* @ORM\InheritanceType("SINGLE_TABLE")
* @ORM\DiscriminatorColumn(name="refund_type", type="string")
* @ORM\DiscriminatorMap( {"typeA_in_database" = "ChildEntityA", "typeB_in_database" = "ChildEntityB" } )
*/
abstract class Base
{
--------------------------------------
---------------------------------------
Child entity class
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Validator\Constraints as Assert;
use DDRCBundle\Validator\FilesTotalSize;
/**
* @ORM\Entity
* Child
*
*/
class Child extends Base
Cristian Pana - personal blog on software development, PHP, Symfony Framework, web technologies.
Showing posts with label entities. Show all posts
Showing posts with label entities. Show all posts
Friday, January 8, 2016
Sunday, November 1, 2015
Generate Entities from an Existing Database with Symfony and Doctrine
As said in a previous post, I am trying to see how easy is to create a Symfony bundle starting from an existing Database (like when you want to migrate an old app to Symfony). From Symfony documentation:
Below the steps I made to generate entities from existing tables in database:
1. Change current path to home directory of my Symfony installation.
Execute this command:
php app/console doctrine:mapping:import --force CPANAClassifiedsBundle xml
The metadata files are generated under "/src/CPANAClassifiedsBundle/Resources/config/doctrine/". Delete .orm.xml files related to other tables than the ones related to your bundle
2. Once the metadata files are generated, you can ask Doctrine to build related entity classes by executing the following two commands.
php app/console doctrine:mapping:convert annotation ./src/CPANA/ClassifiedsBundle
check if the data was exported into the correct path otherwise copy it to Entity directory.
php app/console doctrine:generate:entities CPANAClassifiedsBundle
NOTE: If you want to have a one-to-many relationship, you will need to add it manually into the entity or to the generated XML or YAML files. Add a section on the specific entities for one-to-many defining the inversedBy and the mappedBy pieces.
http://stackoverflow.com/questions/12493865/what-is-the-difference-between-inversedby-and-mappedby
3. Delete the mapping generated by Doctrine in the directory "/src/CPANA/ClassifiedsBundle/Resources/config/doctrine".
You may need to clear the cache:
/**
* @var \AppBundle\Entity\User
*
* @ORM\ManyToOne(targetEntity="AppBundle\Entity\User")
* @ORM\JoinColumns({
* @ORM\JoinColumn(name="id_user", referencedColumnName="id")
* })
*/
private $idUser;
and
public function setIdUser(\AppBundle\Entity\User $idUser = null)
1. Add a new route to routing.yml
cpana_classifieds_categories_index:
path: /classifieds/categories
defaults: { _controller: CPANAClassifiedsBundle:Categories:index }
2. Add controller class CategoriesController.php:
class CategoriesController extends Controller
{
public function indexAction()
{
$em = $this->getDoctrine()->getEntityManager();
$allCategories = $em->getRepository('CPANAClassifiedsBundle:Category')->findAll();
if (!$allCategories) {
throw $this->createNotFoundException('Unable to find categories.');
}
return $this->render('CPANAClassifiedsBundle:Categories:index.html.twig', array( 'all_cat' => $allCategories ) );
}
}
3. Add the index.html.twig view:
{% extends 'CPANAClassifiedsBundle::layout.html.twig' %}
{% block content %}
<table style="width:100%">
{% for category in all_cat %}
<tr>
<td>{{ category.getIdCategory() }}</td>
<td>{{ category.getCategoryName() }}</td>
{% if category.getIdParent() != false %}
<td>{{ category.getIdParent().getCategoryName() }}</td>
{% else %}
<td>root category</td>
{% endif %}
</tr>
{% endfor %}
</table>
{% endblock %}
--------------------------------------------------------------------------------------------------------------
Some explanations:
The foreign keys from my tables are transformed in objects holding the entire row for that foreign key id. Results from this that category.getIdParent() is returning an object. If you try to display that object you will get an error like "Catchable Fatal Error: "Object of class CPANA\ClassifiedsBundle\Entity\Category could not be converted to string"
So I will be displaying a certain property of that object, let's say CategoryName using the getter: "category.getIdParent().getCategoryName() ".
I had a problem because Doctrine transformed the column "parent_id" into "parent" field which is not good cause "parent" is a key word used by PHP. I had to alter the table and generate again the entities. I suppose Doctrine transformed "parent_id" to "parent" because of the default mapping logic:
As the Doctrine tools documentation says, reverse engineering is a one-time process to get started on a project. Doctrine is able to convert approximately 70-80% of the necessary mapping information based on fields, indexes and foreign key constraints. Doctrine can't discover inverse associations, inheritance types, entities with foreign keys as primary keys or semantical operations on associations such as cascade or lifecycle events. Some additional work on the generated entities will be necessary afterwards to design each to fit your domain model specificities.
Below the steps I made to generate entities from existing tables in database:
1. Change current path to home directory of my Symfony installation.
Execute this command:
php app/console doctrine:mapping:import --force CPANAClassifiedsBundle xml
The metadata files are generated under "/src/CPANAClassifiedsBundle/Resources/config/doctrine/". Delete .orm.xml files related to other tables than the ones related to your bundle
2. Once the metadata files are generated, you can ask Doctrine to build related entity classes by executing the following two commands.
php app/console doctrine:mapping:convert annotation ./src/CPANA/ClassifiedsBundle
check if the data was exported into the correct path otherwise copy it to Entity directory.
php app/console doctrine:generate:entities CPANAClassifiedsBundle
NOTE: If you want to have a one-to-many relationship, you will need to add it manually into the entity or to the generated XML or YAML files. Add a section on the specific entities for one-to-many defining the inversedBy and the mappedBy pieces.
http://stackoverflow.com/questions/12493865/what-is-the-difference-between-inversedby-and-mappedby
3. Delete the mapping generated by Doctrine in the directory "/src/CPANA/ClassifiedsBundle/Resources/config/doctrine".
You may need to clear the cache:
php app/console doctrine:cache:clear-metadataAlso you need to modify the Ads entity to correctly point to your User entity. In my case:
/**
* @var \AppBundle\Entity\User
*
* @ORM\ManyToOne(targetEntity="AppBundle\Entity\User")
* @ORM\JoinColumns({
* @ORM\JoinColumn(name="id_user", referencedColumnName="id")
* })
*/
private $idUser;
and
public function setIdUser(\AppBundle\Entity\User $idUser = null)
Testing how the model classes (entities) generated by Doctrine work
1. Add a new route to routing.yml
cpana_classifieds_categories_index:
path: /classifieds/categories
defaults: { _controller: CPANAClassifiedsBundle:Categories:index }
2. Add controller class CategoriesController.php:
class CategoriesController extends Controller
{
public function indexAction()
{
$em = $this->getDoctrine()->getEntityManager();
$allCategories = $em->getRepository('CPANAClassifiedsBundle:Category')->findAll();
if (!$allCategories) {
throw $this->createNotFoundException('Unable to find categories.');
}
return $this->render('CPANAClassifiedsBundle:Categories:index.html.twig', array( 'all_cat' => $allCategories ) );
}
}
3. Add the index.html.twig view:
{% extends 'CPANAClassifiedsBundle::layout.html.twig' %}
{% block content %}
<table style="width:100%">
{% for category in all_cat %}
<tr>
<td>{{ category.getIdCategory() }}</td>
<td>{{ category.getCategoryName() }}</td>
{% if category.getIdParent() != false %}
<td>{{ category.getIdParent().getCategoryName() }}</td>
{% else %}
<td>root category</td>
{% endif %}
</tr>
{% endfor %}
</table>
{% endblock %}
--------------------------------------------------------------------------------------------------------------
Some explanations:
The foreign keys from my tables are transformed in objects holding the entire row for that foreign key id. Results from this that category.getIdParent() is returning an object. If you try to display that object you will get an error like "Catchable Fatal Error: "Object of class CPANA\ClassifiedsBundle\Entity\Category could not be converted to string"
So I will be displaying a certain property of that object, let's say CategoryName using the getter: "category.getIdParent().getCategoryName() ".
I had a problem because Doctrine transformed the column "parent_id" into "parent" field which is not good cause "parent" is a key word used by PHP. I had to alter the table and generate again the entities. I suppose Doctrine transformed "parent_id" to "parent" because of the default mapping logic:
5.11. Mapping Defaults
The @JoinColumn and @JoinTable definitions are usually optional and have sensible default values.
The defaults for a join column in a one-to-one/many-to-one association is as follows:
name: "<fieldname>_id"
referencedColumnName: "id"
As an example, consider this mapping:
PHP
<?php
/** @OneToOne(targetEntity="Shipping") **/
private $shipping;
XML
YAML
This is essentially the same as the following, more verbose, mapping:
PHP
<?php
/**
* @OneToOne(targetEntity="Shipping")
* @JoinColumn(name="shipping_id", referencedColumnName="id")
**/
private $shipping;
Subscribe to:
Posts (Atom)