table des matières
Avouons-le, l'une des tâches les plus courantes pour toute application implique la persistance et la lecture des informations vers et à partir d'une base de données. Heureusement, Symfony vient intégré avec la doctrine , une bibliothèque dont le seul but est de vous donner des outils puissants pour rendre cela facile. Dans ce chapitre, vous apprendrez la philosophie de base derrière la doctrine et de voir comment facile de travailler avec une base de données peut être.
Doctrine est totalement découplée à partir de Symfony et son utilisation est facultative. Ce chapitre est tout au sujet de la Doctrine ORM, qui vise à vous permettent de mapper des objets à une base de données relationnelle (comme MySQL, PostgreSQL ou Microsoft SQL). Si vous préférez utiliser des requêtes de bases de données brutes, c'est facile, et expliquées dans le Comment faire pour utiliser la couche DBAL Doctrine Vous pouvez également conserver les données à MongoDB en utilisant Doctrine ODM bibliothèque. Pour plus d'informations, lire la documentation DoctrineMongoDBBundle .
La meilleure façon de comprendre comment fonctionne la doctrine, c'est de voir en action. Dans cette section, vous allez configurer votre base de données, créer un objet produit, il persiste à la base de données et chercher arrière.
Si vous voulez suivre l'exemple dans ce chapitre, créer un AcmeStoreBundle via:
php app/console generate:bundle --namespace=Acme/StoreBundle
Avant de vraiment commencer, vous aurez besoin de configurer vos informations de connexion base de données. Par convention, cette information est généralement configuré dans une app/config/parameters.ini
;app/config/parameters.ini [parameters] database_driver = pdo_mysql database_host = localhost database_name = test_project database_user = root database_password = password
La définition de la configuration via parameters.ini n'est qu'une convention. Les paramètres définis dans ce fichier sont référencés par le fichier de configuration principal lors de la mise en place Doctrine:
doctrine: dbal: driver: %database_driver% host: %database_host% dbname: %database_name% user: %database_user% password: %database_password%
En séparant les informations de base de données dans un fichier séparé, vous pouvez facilement conserver les différentes versions du fichier sur chaque serveur. Vous pouvez aussi facilement stocker la configuration de base de données (ou toute autre information sensible) à l'extérieur de votre projet, comme l'intérieur de votre configuration d'Apache, par exemple. Pour plus d'informations, voir comment définir les paramètres externes dans le Container Service .
Maintenant que la doctrine sait au sujet de votre base de données, vous devez l'avoir créer la base de données pour vous:
php app/console doctrine:database:create
Supposons que vous construisez une application où les produits doivent être affichées. Sans même penser Doctrine ou bases de données, vous savez déjà que vous avez besoin d'un objet produit pour représenter ces produits. Créer cette classe à l'intérieur du répertoire de votre entité AcmeStoreBundle:
mkdir src/Acme/StoreBundle/Entity
// src/Acme/StoreBundle/Entity/Product.php namespace Acme\StoreBundle\Entity; class Product { protected $name; protected $price; protected $description; }
La classe - souvent appelée une «entité», ce qui signifie une classe de base qui contient les données - est simple et permet de satisfaire à l'exigence d'avoir besoin de produits dans votre application. Cette classe ne peut pas être conservées dans une base de données encore - c'est juste une classe PHP simple.
Une fois que vous apprennez les concepts sous-jacents Doctrine, vous pouvez avoir Doctrine créer cette classe d'entité pour vous:
php app/console doctrine:generate:entity --entity="AcmeStoreBundle:Product" --fields="name:string(255) price:float description:text"
Doctrine vous permet de travailler avec des bases de données d'une manière beaucoup plus intéressante que de simplement récupérer les lignes d'une colonne de table à base dans un tableau. Au lieu de cela, la doctrine permet également de conserver des objets entiers à la base de données et de chercher des objets entiers sur la base de données. Cela fonctionne en mappant une classe PHP pour une table de base de données, et les propriétés de cette classe PHP pour les colonnes sur la table:
Pour que Doctrine soit capable de faire cela, il vous suffit de créer des «métadonnées», ou des configurations qui indiquent exactement comment la doctrine de la classe du produit et ses propriétés doivent être mis en correspondance avec la base de données. Ces métadonnées peuvent être spécifiées dans un certain nombre de formats différents, y compris YAML, XML ou directement à l'intérieur de la classe du produit via les annotations:
Un bundle peut accepter qu'un seul format de définition des métadonnées. Par exemple, il n'est pas possible de mélanger les définitions de métadonnées YAML avec PHP définitions annotés classe d'entité.
// src/Acme/StoreBundle/Entity/Product.php namespace Acme\StoreBundle\Entity; use Doctrine\ORM\Mapping as ORM; /** * @ORM\Entity * @ORM\Table(name="product") */ class Product { /** * @ORM\Id * @ORM\Column(type="integer") * @ORM\GeneratedValue(strategy="AUTO") */ protected $id; /** * @ORM\Column(type="string", length=100) */ protected $name; /** * @ORM\Column(type="decimal", scale=2) */ protected $price; /** * @ORM\Column(type="text") */ protected $description; }
# src/Acme/StoreBundle/Resources/config/doctrine/Product.orm.yml Acme\StoreBundle\Entity\Product: type: entity table: product id: id: type: integer generator: { strategy: AUTO } fields: name: type: string length: 100 price: type: decimal scale: 2 description: type: text
<!-- src/Acme/StoreBundle/Resources/config/doctrine/Product.orm.xml --> <doctrine-mapping xmlns="http://doctrine-project.org/schemas/orm/doctrine-mapping" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://doctrine-project.org/schemas/orm/doctrine-mapping http://doctrine-project.org/schemas/orm/doctrine-mapping.xsd"> <entity name="Acme\StoreBundle\Entity\Product" table="product"> <id name="id" type="integer" column="id"> <generator strategy="AUTO" /> </id> <field name="name" column="name" type="string" length="100" /> <field name="price" column="price" type="decimal" scale="2" /> <field name="description" column="description" type="text" /> </entity> </doctrine-mapping>
Le nom de la table est facultative et si elle est omise, sera déterminé automatiquement en fonction du nom de la classe d'entité.
Doctrine vous permet de choisir parmi une grande variété de types de champs différents, chacun avec leurs propres options. Pour plus d'informations sur les types de champs disponibles, voir Cartographie de base Symfony2
Si vous utilisez les annotations, vous aurez besoin de faire précéder toutes les annotations avec ORM\ (e.g. ORM\Column(..)), qui n'est pas représenté dans la documentation de Doctrine. Vous aurez également besoin d'inclure Doctrine\ORM\Mapping as ORM; déclaration, qui comporte le préfixe annotations ORM.
Veillez à ce que votre nom de classe et les propriétés ne sont pas mappés à un mot-clé protégé SQL (tels que le groupe ou l'utilisateur). Par exemple, si votre nom de classe d'entité est le groupe, puis, par défaut, le nom de votre table sera le groupe, ce qui provoquera une erreur SQL dans certains moteurs. Voir Cartographie de base SQL mots-clés de documentation sur la façon de bien échapper ces noms
Lorsque vous utilisez une autre bibliothèque ou d'un programme (c.-à-Doxygen) qui utilise les annotations, vous devez placer l'annotation @IgnoreAnnotation sur la classe pour indiquer ce que Symfony annotations doit ignorer.
Par exemple, pour empêcher l'annotation @fn de lancer une exception, ajoutez la ligne suivante:
/** * @IgnoreAnnotation("fn") */ class Product
Même si la doctrine sait maintenant comment persister un objet produit à la base de données, la classe elle-même n'est pas vraiment utile pour le moment. Comme le produit est tout simplement une classe ordinaire PHP, vous avez besoin de créer des méthodes getter et setter (par exemple getName(), setName()) afin d'accéder à ses propriétés (puisque les propriétés sont protégés). Heureusement, la doctrine peut le faire pour vous en exécutant:
php app/console doctrine:generate:entities Acme/StoreBundle/Entity/Product
Cette commande permet de s'assurer que tous les getters et setters sont générés pour la classe du produit. Il s'agit d'une commande en toute sécurité - vous pouvez l'exécuter maintes et maintes fois: il ne génère que des accesseurs qui n'existent pas (c'est à dire qu'il ne remplace pas vos méthodes existantes).
En savoir plus sur doctrine:generate:entities
Avec la commande doctrine:generate:entities, vous pouvez:
@ORM\Entity(repositoryClass="...")
La commande doctrine:generate:entities enregistre une sauvegarde de la product.php originale nommée product.php~. Dans certains cas, la présence de ce fichier peut provoquer une "Cannot redeclare class" d'erreur. Il peut être retiré en toute sécurité.
Notez que vous n'avez pas besoin d'utiliser cette commande. Doctrine ne repose pas sur la génération de code. Comme avec les classes PHP normales, il vous suffit de vous assurer que vos propriétés protégés/privé sont getter et setter. Puisqu'il s'agit d'une chose commune à faire lors de l'utilisation Doctrine, cette commande a été créé.
Vous pouvez également générer toutes les entités connues (c'est à dire toute la classe PHP avec les informations de mappage Doctrine) d'un faisceau ou un espace de noms ensemble:
php app/console doctrine:generate:entities AcmeStoreBundle php app/console doctrine:generate:entities Acme
Doctrine ne se soucie pas de savoir si vos propriétés sont protégés ou privés, ou si vous avez ou non un getter ou un setter pour la fonction d'une propriété. Les getters et setters sont générés ici seulement parce que vous avez besoin d'interagir avec votre objet PHP.
Vous avez maintenant une classe de produit utilisable avec les informations de mappage de telle sorte que la doctrine ne sait exactement comment elle devait persister. Bien sûr, vous n'avez pas encore de la table des produits correspondant à votre base de données. Heureusement, la doctrine peut créer automatiquement toutes les tables de bases de données nécessaires pour chaque entité connue dans votre application. Pour ce faire, exécutez:
php app/console doctrine:schema:update --force
En fait, cette commande est incroyablement puissante. Elle compare ce que votre base de données devrait ressembler (basé sur les informations de mappage de vos entités) à la façon dont cela semble effectivement, et génère les instructions SQL nécessaires pour mettre à jour la base de données à l'endroit où elle devrait être. En d'autres termes, si vous ajoutez une nouvelle propriété avec des métadonnées de cartographie pour des produits et exécuter de nouveau cette tâche, il génére la déclaration "alter table" nécessaire à ajouter pour que la nouvelle colonne de la table des produits existe.
Une façon encore meilleure pour profiter de cette fonctionnalité est par les migrations, qui vous permettent de générer ces instructions SQL et de les stocker dans des classes de migration qui peuvent être exécutées systématiquement sur votre serveur de production afin de suivre et de migrer votre schéma de base de manière sûre et fiable.
Votre base de données dispose désormais d'un tableau produit entièrement fonctionnel avec des colonnes qui correspondent aux métadonnées que vous avez spécifié.
Maintenant que vous avez une entité produit mappé et table produit correspondant, vous êtes prêt à conserver les données de la base de données. De l'intérieur d'un contrôleur, c'est assez facile. Ajoutez la méthode suivante à DefaultController du bundle:
1 // src/Acme/StoreBundle/Controller/DefaultController.php 2 use Acme\StoreBundle\Entity\Product; 3 use Symfony\Component\HttpFoundation\Response; 4 // ... 5 6 public function createAction() 7 { 8 $product = new Product(); 9 $product->setName('A Foo Bar'); 10 $product->setPrice('19.99'); 11 $product->setDescription('Lorem ipsum dolor'); 12 13 $em = $this->getDoctrine()->getEntityManager(); 14 $em->persist($product); 15 $em->flush(); 16 17 return new Response('Created product id '.$product->getId()); 18 }
Si vous suivez cet exemple, vous aurez besoin pour créer un itinéraire qui pointe vers cette action pour voir comment il fonctionne.
Regardons cet exemple:
En fait, Doctrine est au courant de toutes vos entités gérées, lorsque vous appelez la méthode flush(), il calcule un ensemble de modifications global et exécute la requête la plus efficace / requêtes possibles. Par exemple, si vous persistez un total de 100 objets sur le produit et ensuite appeler flush(), la doctrine va créer une seule instruction préparée et la ré-utiliser pour chaque insert. Ce modèle est appelé unité de travail, et il est utilisé parce qu'il est rapide et efficace.
Lors de la création ou la mise à jour des objets, le processus est toujours le même. Dans la section suivante, vous verrez comment la doctrine est assez intelligente pour émettre automatiquement une requête UPDATE si le dossier existe déjà dans la base de données.
Doctrine fournit une bibliothèque qui vous permet de charger par programme les données de test dans votre projet («données de test" par exemple). Pour plus d'informations, reportez-vous DoctrineFixturesBundle .
La comparaison d'un objet en arrière sur la base de données est encore plus facile. Par exemple, supposons que vous avez configuré un itinéraire pour afficher un produit spécifique, basé sur sa valeur id:
public function showAction($id) { $product = $this->getDoctrine() ->getRepository('AcmeStoreBundle:Product') ->find($id); if (!$product) { throw $this->createNotFoundException('No product found for id '.$id); } // do something, like pass the $product object into a template }
Lorsque vous interrogez pour un type d'objet particulier, vous utilisez toujours ce qu'on appelle son "référentiel". Vous pouvez penser à un dépôt comme une classe PHP dont le seul travail consiste à vous aider à récupérer les entités d'une certaine classe. Vous pouvez accéder à l'objet référentiel pour une classe d'entité par l'intermédiaire:
$repository = $this->getDoctrine() ->getRepository('AcmeStoreBundle:Product');
La chaîne AcmeStoreBundle:Product est un raccourci que vous pouvez utiliser n'importe où dans Doctrine au lieu du nom complet de la classe de l'entité (c.-à-d Acme\StoreBundle\Entity\Product). Tant que vos entité existe dans l'espace de noms de votre paquet, cela va fonctionner.
Une fois que vous avez votre référentiel, vous avez accès à toutes sortes de méthodes utiles:
// query by the primary key (usually "id") $product = $repository->find($id); // dynamic method names to find based on a column value $product = $repository->findOneById($id); $product = $repository->findOneByName('foo'); // find *all* products $products = $repository->findAll(); // find a group of products based on an arbitrary column value $products = $repository->findByPrice(19.99);
Bien sûr, vous pouvez également effectuer des requêtes complexes, vous en apprendrez davantage sur la section Interrogation des objets de l'article.
Vous pouvez également profiter des méthodes findBy et findOneBy utiles facilement aller chercher des objets en fonction de plusieurs conditions:
// query for one product matching be name and price $product = $repository->findOneBy(array('name' = > 'foo', 'price' = > 19.99)); // query for all products matching the name, ordered by price $product = $repository->findBy( array('name' = > 'foo'), array('price' = > 'ASC') );
Lorsque vous rendre n'importe quelle page, vous pouvez voir combien de requêtes ont été faites dans le coin en bas à droite de la barre d'outils de web debug.
Si vous cliquez sur l'icône, le profileur va s'ouvrir, vous montrant les requêtes exactes qui ont été faites.
Une fois que vous avez récupéré un objet de la doctrine, la mise à jour, est facile. Supposons que vous ayez une route qui mappe un identifiant qui produit une action de mise à jour dans un contrôleur:
public function updateAction($id) { $em = $this->getDoctrine()->getEntityManager(); $product = $em->getRepository('AcmeStoreBundle:Product')->find($id); if (!$product) { throw $this->createNotFoundException('No product found for id '.$id); } $product->setName('New product name!'); $em->flush(); return $this->redirect($this->generateUrl('homepage')); }
Notez que l'appel de $em->persist($product) n'est pas nécessaire. Rappelons que cette méthode indique simplement que la doctrine gére ou regarde l'objet $product. Dans ce cas, puisque vous chercher l'objet $product à partir de Doctrine, il est déjà géré.
Suppression d'un objet est très similaire, mais nécessite un appel à la méthode remove() du gestionnaire de l'entité:
$em->remove($product); $em->flush();
Comme on pouvait s'y attendre, la méthode remove() avise la doctrine que vous souhaitez supprimer l'entité donnée à partir de la base de données. La requête DELETE réelle, cependant, n'est réellement exécutée que lorsque la méthode flush() est appelée.
Vous avez déjà vu comment l'objet référentiel vous permet d'exécuter des requêtes de base sans aucun travail:
$repository->find($id); $repository->findOneByName('Foo');
Bien sûr, Doctrine vous permet également d'écrire des requêtes plus complexes en utilisant Doctrine Query Language (DQL). DQL est similaire à SQL, sauf que vous devez imaginer que vous vous interrogez pour un ou plusieurs objets d'une classe d'entité (produit, par exemple) au lieu d'interroger des lignes sur une table (produit par exemple).
Lors de l'interrogation dans Doctrine, vous avez deux options: l'écriture de requêtes pure doctrine ou l'utilisation du Générateur de requêtes de Doctrine.
Imaginez que vous voulez interroger pour les produits, mais seulement retourner les produits qui coûtent plus de 19,99, et ordonné su moins cher au plus cher. De l'intérieur d'un contrôleur, procédez comme suit:
$em = $this->getDoctrine()->getEntityManager(); $query = $em->createQuery( 'SELECT p FROM AcmeStoreBundle:Product p WHERE p.price > :price ORDER BY p.price ASC' )->setParameter('price', '19.99'); $products = $query->getResult();
Si vous êtes à l'aise avec SQL, DQL doit se sentir très naturel. La plus grande différence est que vous devez penser en termes d' «objets» au lieu de lignes dans une base de données. Pour cette raison, vous sélectionnez à partir AcmeStoreBundle:Product et ensuite d'un alias comme p.
La méthode getResult() retourne un tableau des résultats. Si vous vous interrogez pour un seul objet, vous pouvez utiliser la getSingleResult()
$product = $query->getSingleResult();
La méthode getSingleResult() lève une exception Doctrine\ORM\NoResultException si aucun résultat n'est retourné et une octrine\ORM\NonUniqueResultException si plus d'un résultat est retourné. Si vous utilisez cette méthode, vous devrez peut-être de l'envelopper dans un bloc try-catch et s'assurer qu'un seul résultat est retourné (si vous vous interrogez sur quelque chose qui permet d'envisager quelle peut renvoyer plus d'un résultat):
$query = $em->createQuery('SELECT ....') ->setMaxResults(1); try { $product = $query->getSingleResult(); } catch (\Doctrine\Orm\NoResultException $e) { $product = null; } // ...
La syntaxe DQL est incroyablement puissante, vous permettant de joindre facilement des entités (le sujet des relations sera traité plus tard), groupe, etc Pour plus d'informations, consultez la doctrine officielle Doctrine Query Language Symfony2
Prenez note de la méthode setParameter(). Lorsque vous travaillez avec Doctrine, c'est toujours une bonne idée de mettre toutes les valeurs externes en tant que "placeholders", qui a été fait dans la requête ci-dessus:
... WHERE p.price > :price ...
Vous pouvez ensuite définir la valeur de l'espace réservé prix en appelant la méthode setParameter()
->setParameter('price', '19.99')
Utilisation de paramètres au lieu de placer les valeurs directement dans la chaîne de requête est fait pour empêcher les attaques par injection SQL et devrait toujours être fait. Si vous utilisez plusieurs paramètres, vous pouvez définir leurs valeurs à la fois en utilisant les méthodes setParameters()
->setParameters(array( 'price' = > '19.99', 'name' = > 'Foo', ))
Au lieu d'écrire des requêtes directement, vous pouvez également utiliser QueryBuilder Doctrine qui fait le même travail en utilisant une belle interface orientée objet. Si vous utilisez un IDE, vous pouvez également profiter de l'auto-complétion pour tapez les noms des méthodes. De l'intérieur d'un contrôleur:
$repository = $this->getDoctrine() ->getRepository('AcmeStoreBundle:Product'); $query = $repository->createQueryBuilder('p') ->where('p.price > :price') ->setParameter('price', '19.99') ->orderBy('p.price', 'ASC') ->getQuery(); $products = $query->getResult();
L'objet contient tous les QueryBuilder méthode nécessaire pour créer votre requête. En appelant la méthode getQuery(), le constructeur de requêtes retourne un objet de requête normale, qui est l'objet même que vous avez construit directement dans la section précédente.
Pour plus d'informations sur les Doctrine Query Builder, consulter Le QueryBuilder Symfony2
Dans les sections précédentes, vous avez commencé à construire et utiliser des requêtes plus complexes à l'intérieur d'un contrôleur. Afin d'isoler, de tester et de réutiliser ces requêtes, c'est une bonne idée de créer une classe de stockage personnalisé pour votre entité et ajouter des méthodes avec votre logique de la requête.
Pour ce faire, ajoutez le nom de la classe de référentiel pour la définition de votre cartographie.
// src/Acme/StoreBundle/Entity/Product.php namespace Acme\StoreBundle\Entity; use Doctrine\ORM\Mapping as ORM; /** * @ORM\Entity(repositoryClass="Acme\StoreBundle\Repository\ProductRepository") */ class Product { //... }
# src/Acme/StoreBundle/Resources/config/doctrine/Product.orm.yml Acme\StoreBundle\Entity\Product: type: entity repositoryClass: Acme\StoreBundle\Repository\ProductRepository # ...
<!-- src/Acme/StoreBundle/Resources/config/doctrine/Product.orm.xml --> <!-- ... --> <doctrine-mapping> <entity name="Acme\StoreBundle\Entity\Product" repository-class="Acme\StoreBundle\Repository\ProductRepository"> <!-- ... --> </entity> </doctrine-mapping>
Ensuite, ajoutez une nouvelle méthode - findAllOrderedByName() - à la classe référentiel nouvellement généré. Cette méthode va interroger l'ensemble des entités du produit, par ordre alphabétique.
mkdir src/Acme/StoreBundle/Repository
// src/Acme/StoreBundle/Repository/ProductRepository.php namespace Acme\StoreBundle\Repository; use Doctrine\ORM\EntityRepository; class ProductRepository extends EntityRepository { public function findAllOrderedByName() { return $this->getEntityManager() ->createQuery('SELECT p FROM AcmeStoreBundle:Product p ORDER BY p.name ASC') ->getResult(); } }
La doctrine peut générer la classe référentielle pour vous en exécutant la commande même que celle utilisée précédemment pour générer les méthodes manquantes getter et setter:
php app/console doctrine:generate:entities Acme
Le gestionnaire d'entités peuvent être accessibles via $this->getEntityManager() à l'intérieur du dépôt.
Vous pouvez utiliser cette nouvelle méthode, tout comme les méthodes de recherche par défaut du référentiel:
$em = $this->getDoctrine()->getEntityManager(); $products = $em->getRepository('AcmeStoreBundle:Product') ->findAllOrderedByName();
Lorsque vous utilisez une classe de stockage personnalisé, vous avez toujours accès à des méthodes de recherche par défaut tels que find() et findAll().
Supposons que les produits dans votre application appartiennent tous à exactement une «catégorie». Dans ce cas, vous aurez besoin d'un objet Catégorie et une façon de relier un objet produit à un objet Catégorie. Commencez par créer l'entité Catégorie. Puisque vous savez que vous aurez éventuellement besoin de persister dans la classe Doctrine, vous pouvez laisser Doctrine créer la classe pour vous.
php app/console doctrine:generate:entity --entity="AcmeStoreBundle:Category" --fields="name:string(255)"
Cette tâche génère l'entité Catégorie pour vous, avec un champ id, un champ nom et les fonctions associées getter et setter.
Pour relier les entités Catégorie et produit, commencez par créer une propriété des produits de la classe Catégorie:
// src/Acme/StoreBundle/Entity/Category.php // ... use Doctrine\Common\Collections\ArrayCollection; class Category { // ... /** * @ORM\OneToMany(targetEntity="Product", mappedBy="category") */ protected $products; public function __construct() { $this->products = new ArrayCollection(); } }
# src/Acme/StoreBundle/Resources/config/doctrine/Category.orm.yml Acme\StoreBundle\Entity\Category: type: entity # ... oneToMany: products: targetEntity: Product mappedBy: category # don't forget to init the collection in entity __construct() method
Tout d'abord, depuis un objet de sa catégorie se rapportent à des objets produit de nombreux, une propriété de tableau des produits est ajouté à détenir ces objets Product. Encore une fois, ce n'est pas fait parce que la doctrine dont il a besoin, mais plutôt parce qu'il fait sens dans l'application pour chaque catégorie de tenir un tableau d'objets du produit.
Le code dans la méthode __construct() est important parce que la doctrine exige que la propriété $products soit un objet ArrayCollection. Cet objet ressemble et agit presque exactement comme un tableau, mais possède une certaine souplesse supplémentaire. Si cela vous rend mal à l'aise, ne vous inquiétez pas. Imaginez que c'est un tableau et tout ira bien.
La valeur targetEntity dans le décorateur utilisé ci-dessus peut faire référence à toute entité, avec un espace de noms valide, et pas seulement les entités définies dans la même classe. Si ils se rapportent à une entité définie dans une classe ou un bundle différent, entrez un espace de noms complet comme le targetEntity.
Ensuite, puisque chaque classe de produit peut se rapporter à un seul objet Catégorie, vous aurez envie d'ajouter une propriété $catégory à la classe du produit:
// src/Acme/StoreBundle/Entity/Product.php // ... class Product { // ... /** * @ORM\ManyToOne(targetEntity="Category", inversedBy="products") * @ORM\JoinColumn(name="category_id", referencedColumnName="id") */ protected $category; }
# src/Acme/StoreBundle/Resources/config/doctrine/Product.orm.yml Acme\StoreBundle\Entity\Product: type: entity # ... manyToOne: category: targetEntity: Category inversedBy: products joinColumn: name: category_id referencedColumnName: id
Enfin, maintenant que vous avez ajouté une nouvelle propriété à la fois à la catégorie et aux classes de produits, dites à Doctrine de générer le getter et setter qui manque pour vous:
php app/console doctrine:generate:entities Acme
Ignorer les métadonnées Doctrine pour un moment. Vous avez maintenant deux classes - catégorie et produit avec un naturel un-à-plusieurs. La classe Catégorie titulaire d'un tableau d'objets produit et l'objet produit peut contenir un objet Category. En d'autres termes vous avez construit vos classes d'une manière qui fait sens pour vos besoins. Le fait que les données doivent être conservées dans une base de données est toujours secondaire.
Maintenant, regardez les métadonnées issut de la propriété $catégory sur la classe du produit. L'information ici indique à doctrine que la classe est liée à la catégorie et qu'il doit stocker l'id de l'enregistrement de catégorie sur un champ category_id qui vit sur la table des produits. En d'autres termes, les objets catégorie liés seront stockées sur la propriété $catégory, mais dans les coulisses, la doctrine va persister cette relation en stockant la valeur de l'id de la catégorie sur une colonne de la table category_id produit.
Les métadonnées issus de la propriété des produits de l'objet catégorie est moins important, et raconte tout simplement à Doctrine de regarder la propriété Product.category pour comprendre comment la relation est mappée.
Avant de continuer, assurez-vous de dire à Doctrine d'ajouter la table nouvelle catégorie, et la colonne product.category_id, et la nouvelle clé étrangère:
php app/console doctrine:schema:update --force
Cette tâche ne devrait pas être réellement utilisé au cours du développement. Pour une méthode plus robuste de la mise à jour de votre base de données de production, en savoir plus sur les migrations Doctrine .
Maintenant, nous allons voir le code dans l'action. Imaginez que vous êtes à l'intérieur d'un contrôleur:
// ... use Acme\StoreBundle\Entity\Category; use Acme\StoreBundle\Entity\Product; use Symfony\Component\HttpFoundation\Response; // ... class DefaultController extends Controller { public function createProductAction() { $category = new Category(); $category->setName('Main Products'); $product = new Product(); $product->setName('Foo'); $product->setPrice(19.99); // relate this product to the category $product->setCategory($category); $em = $this->getDoctrine()->getEntityManager(); $em->persist($category); $em->persist($product); $em->flush(); return new Response( 'Created product id: '.$product->getId().' and category id: '.$category->getId() ); } }
Maintenant, une seule ligne est ajoutée à la fois la catégorie et tableaux des produits. La colonne product.category_id pour le nouveau produit est mis à tout ce que l'id est de la nouvelle catégorie. Doctrine gère la persistance de cette relation pour vous.
Lorsque vous avez besoin d'aller chercher des objets associés, votre flux de travail semble juste comme il le faisait avant. Tout d'abord, aller chercher un objet $product et ensuite accéder à sa catégorie connexe:
public function showAction($id) { $product = $this->getDoctrine() ->getRepository('AcmeStoreBundle:Product') ->find($id); $categoryName = $product->getCategory()->getName(); // ... }
Dans cet exemple, la première requête pour un objet produit est basé sur l'identification du produit. Cela émet une requête uniquement pour les données sur les produits et les hydrates de l'objet $product avec ces données. Plus tard, lorsque vous appelez $product->getCategory()->getName(), la doctrine fait silencieusement une deuxième requête pour trouver la catégorie qui est relatif à ce produit. Il prépare l'objet $category et il revient à vous.
Ce qui est important est le fait que vous ayez un accès facile à la catégorie du produit lié, mais la catégorie de données n'est pas réellement extraites jusqu'à ce que vous demandez pour la catégorie (c'est à dire qu'il est "chargé paresseusement" ( lazily loaded ) ).
Vous pouvez également interroger dans l'autre sens:
public function showProductAction($id) { $category = $this->getDoctrine() ->getRepository('AcmeStoreBundle:Category') ->find($id); $products = $category->getProducts(); // ... }
Dans ce cas, les mêmes choses se produisent: vous devez d'abord interroger sur un objet Catégorie unique, puis la doctrine fait une deuxième requête pour récupérer les objets liés aux produits, mais seulement une fois ou si vous en faîte la demande (c'est à dire quand vous appelez ->getProducts()). La variable $products est un tableau de tous les objets qui se rapportent à des produits de l'objet Catégorie donné via leur valeur category_id.
Cette "lazy loading" est possible parce que, lorsque cela est nécessaire, la doctrine renvoie un "proxy" objet à la place de l'objet véritable. Regardez à nouveau l'exemple ci-dessus:
$product = $this->getDoctrine() ->getRepository('AcmeStoreBundle:Product') ->find($id); $category = $product->getCategory(); // prints "Proxies\AcmeStoreBundleEntityCategoryProxy" echo get_class($category);
Cet objet proxy étend l'objet Catégorie vrai, et ressemble et agit exactement comme elle. La différence est que, en utilisant un objet proxy, Doctrine peut retarder l'interrogation des données Catégorie réels jusqu'à ce que vous avez réellement besoin de ces données (par exemple jusqu'à ce que vous appelez $category->getName()).
Les classes proxy sont générés par la doctrine et stockés dans le répertoire de cache. Et vous n'aurez probablement jamais remarqué que votre objet est en fait la $catégorie d'un objet proxy, il est important de garder à l'esprit.
Dans la section suivante, lorsque vous récupérez les données sur les produits et la catégorie à la fois (via une jointure), Doctrine retournera l'objet Catégorie vrai, puisque rien ne doit être chargé paresseusement.
Dans les exemples ci-dessus, deux requêtes ont été faites - un pour l'objet original (par exemple une catégorie) et l'autre pour l'objet lié (s) (par exemple les objets Product).
Rappelez-vous que vous pouvez voir toutes les requêtes faites au cours d'une demande via le web debug toolbar.
Bien sûr, si vous savez d'avance que vous aurez besoin pour accéder aux deux objets, vous pouvez éviter la deuxième requête par l'émission d'une jointure dans la requête originale. Ajoutez la méthode suivante à la classe ProductRepository:
// src/Acme/StoreBundle/Repository/ProductRepository.php public function findOneByIdJoinedToCategory($id) { $query = $this->getEntityManager() ->createQuery(' SELECT p, c FROM AcmeStoreBundle:Product p JOIN p.category c WHERE p.id = :id' )->setParameter('id', $id); try { return $query->getSingleResult(); } catch (\Doctrine\ORM\NoResultException $e) { return null; } }
Maintenant, vous pouvez utiliser cette méthode dans votre contrôleur pour interroger un objet produit et sa catégorie connexe avec une seule requête:
public function showAction($id) { $product = $this->getDoctrine() ->getRepository('AcmeStoreBundle:Product') ->findOneByIdJoinedToCategory($id); $category = $product->getCategory(); // ... }
Cette section a été une introduction à un type commun d'entité-relation, la relation one-to-many. Pour plus de détails plus avancés et des exemples de la façon d'utiliser d'autres types de relations (par exemple, un-to-one, many-to-many), voir la documentation Cartographie Association Symfony2
Si vous utilisez des annotations, vous aurez besoin de faire précéder toutes les annotations avec ORM\ (par exemple ORM\OneToMany), qui n'est pas reflétée dans la documentation de Doctrine. Vous aurez également besoin d'inclure use Doctrine\ORM\Mapping as ORM; déclaration, qui importe le préfixe annotations ORM.
Doctrine est hautement configurable, vous n'aurez probablement jamais besoin de vous inquiéter de la plupart de ses options. Pour en savoir plus sur la configuration de la doctrine, voir la rubrique Doctrine du manuel de référence.
Parfois, vous avez besoin d'effectuer une action juste avant ou après qu'une entité soit insérée, modifiée ou effacée. Ces types d'actions sont connus comme "lifecycle" callbacks, car ce sont des méthodes de callbacks que vous avez besoin pour exécuter différentes étapes du cycle de vie d'une entité (par exemple l'entité est insérée, mise à jour, supprimée, etc.)
Si vous utilisez les annotations pour vos métadonnées, commencer par les lifecycle callbacks. Ce n'est pas nécessaire si vous utilisez YAML ou XML pour votre application:
/** * @ORM\Entity() * @ORM\HasLifecycleCallbacks() */ class Product { // ... }
Maintenant, vous pouvez dire à doctrine d'exécuter une méthode sur l'un des événements du cycle de vie disponibles. Par exemple, supposons que vous souhaitez définir une colonne de date de création à la date actuelle, lorsque l'entité est persisté (c.-à-d. inséré):
/** * @ORM\PrePersist */ public function setCreatedValue() { $this->created = new \DateTime(); }
# src/Acme/StoreBundle/Resources/config/doctrine/Product.orm.yml Acme\StoreBundle\Entity\Product: type: entity # ... lifecycleCallbacks: prePersist: [ setCreatedValue ]
<!-- src/Acme/StoreBundle/Resources/config/doctrine/Product.orm.xml --> <!-- ... --> <doctrine-mapping> <entity name="Acme\StoreBundle\Entity\Product"> <!-- ... --> <lifecycle-callbacks> <lifecycle-callback type="prePersist" method="setCreatedValue" /> </lifecycle-callbacks> </entity> </doctrine-mapping>
L'exemple ci-dessus suppose que vous avez créé et cartographié une propriété créée (non représenté ici).
Maintenant, juste avant que l'entité soit d'abord persisté, Doctrine appelle automatiquement cette méthode et le champ créé sera mis à la date actuelle.
Ceci peut être répété pour l'un des événements du cycle de vie d'autres, qui comprennent:
Pour plus d'informations sur ce ces événements du cycle de vie moyenne et les rappels de cycle de vie en général, voir Evénements Lifecycle
Notez que la méthode setCreatedValue() ne reçoit pas d'arguments. C'est toujours le cas pour les Lifecycle Callbacks et c'est intentionnelle: les lifecycle callbacks devraient être des méthodes simples qui sont concernés par l'intérieur de transformation de données dans l'entité (par exemple la fixation d'un champ créé / mis à jour, générant une valeur slug).
Si vous avez besoin de faire quelque chose de plus conséquent - comme un logging ou envoyer un e-mail - vous devez vous inscrire une classe externe comme un event listener ou subscriber et lui donner accès à toutes les ressources dont vous avez besoin. Pour plus d'informations, consultez Registering Event Listeners and Subscribers.
La doctrine est assez souple, et un certain nombre de tiers extensions sont disponibles qui vous permettent d'effectuer facilement des tâches répétitives et communes sur vos entités. Il s'agit notamment de telle chose comme Sluggable, Timestampable, Loggable, Translatable, et Tree.
Pour plus d'informations sur la façon de trouver et d'utiliser ces extensions, voir l'article sur l'utilisation des extensions
Doctrine est livré avec un grand nombre de types de champs disponibles. Chacune de ces cartes un type de données PHP à un type de colonne spécifique quelle que soit la base de données que vous utilisez. Les types suivants sont pris en charge dans la doctrine:
Pour plus d'informations, voir Cartographie de base
Chaque champ peut avoir une série d'options qui lui sont appliquées. Les options disponibles incluent le type (par défaut à string), le nom, la longueur, unique et nullable. Prenons quelques exemples:
/** * A string field with length 255 that cannot be null * (reflecting the default values for the "type", "length" and *nullable* options) * * @ORM\Column() */ protected $name; /** * A string field of length 150 that persists to an "email_address" column * and has a unique index. * * @ORM\Column(name="email_address", unique=true, length="150") */ protected $email;
fields: # A string field length 255 that cannot be null # (reflecting the default values for the "length" and *nullable* options) # type attribute is necessary in yaml definitions name: type: string # A string field of length 150 that persists to an "email_address" column # and has a unique index. email: type: string column: email_address length: 150 unique: true
Il y a quelques options supplémentaires qui ne figurent pas ici. Pour plus de détails, consultez la documentation Cartographie de base
L'intégration ORM Doctrine2 offre plusieurs commandes de la console sous l'espace de noms doctrine. Pour voir la liste des commandes que vous pouvez exécuter la console sans aucun argument:
php app/console
Une liste des commandes disponibles peut être imprimer, dont beaucoup commencent par la doctrine: prefix. Vous pouvez trouver plus d'informations sur l'une de ces commandes (ou n'importe quelle commande Symfony) en exécutant la commande d'aide. Par exemple, pour obtenir des détails sur doctrine:database:create task, exécutez:
php app/console help doctrine:database:create
Certaines tâches notables ou intéressantes sont les suivantess:
php app/console doctrine:ensure-production-settings --env=prod
Pour être en mesure de charger jeux de données à votre base de données, vous aurez besoin d'avoir le bundle DoctrineFixturesBundle installé. Pour apprendre à le faire, lisez le "DoctrineFixturesBundle"
Avec Doctrine, vous pouvez vous concentrer sur vos objets et leur utilité dans votre application et vous soucier en second de la persistance en base de données. C'est parce que Doctrine vous permet d'utiliser n'importe quel objet PHP pour stocker vos données et s'appuie sur des informations de métadonnées de cartographie pour cartographier les données d'un objet à une table de base de données particulière.
Et même si la doctrine s'articule autour d'un concept simple, il est incroyablement puissant, vous permettant de créer des requêtes complexes et de vous abonner à des événements qui vous permettent de prendre des mesures différentes comme des objets passent par leur cycle de vie de persistance.