Doctrine - more than an ORM

7ea0eec719c20e8d7880bfdb35f78b4e?s=47 alcaeus
October 26, 2018

Doctrine - more than an ORM

When hearing the word "Doctrine", most developers automatically think of the ORM. Behind the scenes there are many more lesser known packages that are essential to their daily development workflow. We'll take a look at these packages, how they support ORM and how we're adapting these packages in the coming months: full migration to PHP 7, strict typing and static analysis help us improve these essential tools that many developers use without knowing they're there.

7ea0eec719c20e8d7880bfdb35f78b4e?s=128

alcaeus

October 26, 2018
Tweet

Transcript

  1. Doctrine More than just an ORM Andreas Braun @alcaeus

  2. https://tsf.team/

  3. Let’s go back in time

  4. None
  5. class Email extends Doctrine_Record { public function setTableDefinition() { $this->hasColumn('id',

    'integer'); $this->hasColumn('email', 'string'); } public function setUp() { $this->hasOne('User', array( 'local' => 'user_id', 'foreign' => 'id' )); } }
  6. $email = new Email(); $email->email = 'doctrine@example.com'; $email->user = new

    User(); $email->save();
  7. None
  8. None
  9. None
  10. None
  11. None
  12. None
  13. None
  14. None
  15. None
  16. A look below the surface

  17. $ composer require doctrine/orm Using version ^2.6 for doctrine/orm Package

    operations: 14 installs, 0 updates, 0 removals - Installing symfony/polyfill-mbstring (v1.9.0) - Installing symfony/console (v4.1.6) - Installing doctrine/instantiator (1.1.0) - Installing doctrine/event-manager (v1.0.0) - Installing doctrine/cache (v1.8.0) - Installing doctrine/dbal (v2.8.0) - Installing doctrine/lexer (v1.0.1) - Installing doctrine/annotations (v1.6.0) - Installing doctrine/reflection (v1.0.0) - Installing doctrine/collections (v1.5.0) - Installing doctrine/persistence (v1.0.1) - Installing doctrine/inflector (v1.3.0) - Installing doctrine/common (v2.9.0) - Installing doctrine/orm (v2.6.2) Writing lock file Generating autoload files
  18. $ composer require symfony/maker-bundle [...] $ ./bin/console make:entity Email created:

    src/Entity/Email.php created: src/Repository/EmailRepository.php Entity generated! Now let's add some fields! New property name (press <return> to stop adding fields): > email Field type (enter ? to see all types) [string]: > Field length [255]: > Can this field be null in the database (nullable) (yes/no) [no]: >
  19. class Email { private $id; private $email; public function __construct(string

    $email) { $this->email = $email; } public function getEmail(): string { return $this->email; } }
  20. /** * @method Email|null find($id, $lockMode = null, $lockVersion =

    null) * @method Email|null findOneBy(array $criteria, array $orderBy = null) * @method Email[] findAll() * @method Email[] findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null) */ class EmailRepository extends ServiceEntityRepository { public function __construct(RegistryInterface $registry) { parent::__construct($registry, Email::class); } }
  21. namespace Doctrine\Common\Persistence; /** * Contract for a Doctrine persistence layer

    ObjectManager class to implement. */ interface ObjectManager {} /** * Contract for a Doctrine persistence layer repository class to implement. */ interface ObjectRepository {} /** * Contract for a Doctrine persistence layer ClassMetadata class to implement. */ interface ClassMetadata {} /** * Interface for proxy classes. */ interface Proxy {}
  22. /** * @ORM\Entity(repositoryClass=EmailRepository::class) */ class Email { /** * @ORM\Id()

    * @ORM\GeneratedValue() * @ORM\Column(type="integer") */ private $id; /** * @ORM\Column(type="string", length=255) */ private $email; }
  23. None
  24. /** * @Annotation * @Target("CLASS") */ final class SuperDuperClass {

    /** * @Required */ public $level; }
  25. public function getSuperDuperClassLevel(string $className): ?int { $reader = new AnnotationReader();

    $classAnnotations = $reader->getClassAnnotations(new ReflectionClass($className)); foreach ($classAnnotations as $annotation) { if ($annotation instanceof SuperDuperClass) { return $annotation->level; } } return null; }
  26. use App\Annotation\SuperDuperClass; use Doctrine\ORM\Mapping as ORM; /** * @SuperDuperClass(level=5) *

    @ORM\Entity(repositoryClass=EmailRepository::class) */ class Email
  27. class User { /** * @ORM\Id() * @ORM\GeneratedValue() * @ORM\Column(type="integer")

    */ private $id; /** * @ORM\Column(type="string", length=255) */ private $username; /** * @ORM\OneToMany(targetEntity=Email::class, mappedBy="user", orphanRemoval=true) */ private $emails; }
  28. /** * @return Collection|Email[] */ public function getEmails(): Collection {

    return $this->emails; } public function addEmail(Email $email): void { if (!$this->emails->contains($email)) { $this->emails[] = $email; $email->setUser($this); } }
  29. None
  30. /** * Returns a word in singular form. * *

    @param string $word The word in plural form. * * @return string The word in singular form. */ public function singularize(string $word) : string; /** * Returns a word in plural form. * * @param string $word The word in singular form. * * @return string The word in plural form. */ public function pluralize(string $word) : string;
  31. None
  32. $ ./bin/console make:migration Success! Next: Review the new migration "src/Migrations/Version20180922042910.php"

    Then: Run the migration with php bin/console doctrine:migrations:migrate See https://symfony.com/doc/current/bundles/DoctrineMigrationsBundle/index.html
  33. None
  34. final class Version20180922042910 extends AbstractMigration { public function up(Schema $schema)

    : void { // ... } public function down(Schema $schema) : void { $this->addSql('ALTER TABLE email DROP FOREIGN KEY FK_E7927C74A76ED395'); $this->addSql('DROP TABLE email'); $this->addSql('DROP TABLE user'); } }
  35. $ ./bin/console doctrine:migrations:migrate Application Migrations Migrating up to 20180922042910 from

    0 ++ migrating 20180922042910 -> CREATE TABLE email [...] -> CREATE TABLE user [...] -> ALTER TABLE email ADD CONSTRAINT [...] ++ migrated (0.17s) ------------------------ ++ finished in 0.17s ++ 1 migrations executed ++ 3 sql queries
  36. None
  37. doctrine: dbal: # configure these for your database server driver:

    'pdo_mysql' server_version: '5.7' charset: utf8mb4 default_table_options: charset: utf8mb4 collate: utf8mb4_unicode_ci url: '%env(resolve:DATABASE_URL)%'
  38. public function getListTableForeignKeysSQL($table) { $table = $this->normalizeIdentifier($table); $table = $this->quoteStringLiteral($table->getName());

    return "SELECT alc.constraint_name, alc.DELETE_RULE, cols.column_name \"local_column\", cols.position, ( SELECT r_cols.table_name FROM user_cons_columns r_cols WHERE alc.r_constraint_name = r_cols.constraint_name AND r_cols.position = cols.position ) AS \"references_table\", ( SELECT r_cols.column_name FROM user_cons_columns r_cols WHERE alc.r_constraint_name = r_cols.constraint_name AND r_cols.position = cols.position ) AS \"foreign_column\" FROM user_cons_columns cols JOIN user_constraints alc ON alc.constraint_name = cols.constraint_name AND alc.constraint_type = 'R' AND alc.table_name = " . $table . " ORDER BY cols.constraint_name ASC, cols.position ASC"; }
  39. - Installing doctrine/lexer (v1.0.1): Loading from cache - Installing doctrine/annotations

    (v1.6.0): Loading from cache - Installing doctrine/event-manager (v1.0.0): Loading from cache - Installing doctrine/collections (v1.5.0): Loading from cache - Installing doctrine/cache (v1.8.0): Loading from cache - Installing doctrine/persistence (v1.0.1): Loading from cache - Installing doctrine/inflector (v1.3.0): Loading from cache - Installing doctrine/common (v2.9.0): Loading from cache - Installing doctrine/instantiator (1.1.0): Loading from cache - Installing doctrine/dbal (v2.8.0): Loading from cache - Installing doctrine/migrations (v1.8.1): Loading from cache - Installing doctrine/orm (v2.6.2): Loading from cache
  40. - Installing doctrine/lexer (v1.0.1): Loading from cache - Installing doctrine/annotations

    (v1.6.0): Loading from cache - Installing doctrine/event-manager (v1.0.0): Loading from cache - Installing doctrine/collections (v1.5.0): Loading from cache - Installing doctrine/cache (v1.8.0): Loading from cache - Installing doctrine/persistence (v1.0.1): Loading from cache - Installing doctrine/inflector (v1.3.0): Loading from cache - Installing doctrine/common (v2.9.0): Loading from cache - Installing doctrine/instantiator (1.1.0): Loading from cache - Installing doctrine/dbal (v2.8.0): Loading from cache - Installing doctrine/migrations (v1.8.1): Loading from cache - Installing doctrine/orm (v2.6.2): Loading from cache
  41. None
  42. /** * @Route("/signup", name="signup") */ public function __invoke(EntityManager $em, string

    $username, string $email): Response { $user = new User($username); $user->addEmail(new Email($email)); $em->persist($user); $em->flush(); return $this->json([], 204); }
  43. /** * @ORM\OneToMany(targetEntity="App\Entity\Email", mappedBy="user", orphanRemoval=true) */ private $emails; public function

    __construct() { $this->emails = new ArrayCollection(); } /** * @return Collection|Email[] */ public function getEmails(): Collection { return $this->emails; } public function addEmail(Email $email): self { if (!$this->emails->contains($email)) { $this->emails[] = $email; $email->setUser($this); } return $this; }
  44. None
  45. interface Collection extends Countable, IteratorAggregate, ArrayAccess { public function add($element);

    public function clear(); public function contains($element); public function isEmpty(); public function remove($key); // ... }
  46. public function takeSnapshot() { $this->snapshot = $this->collection->toArray(); $this->isDirty = false;

    } public function getDeleteDiff() { return array_udiff_assoc( $this->snapshot, $this->collection->toArray(), function($a, $b) { return $a === $b ? 0 : 1; } ); } public function getInsertDiff() { return array_udiff_assoc( $this->collection->toArray(), $this->snapshot, function($a, $b) { return $a === $b ? 0 : 1; } ); }
  47. class UserRepository extends ServiceEntityRepository { public function findOneByUsername(string $username): ?User

    { return $this->createQueryBuilder('u') ->andWhere('u.username = :username') ->setParameter('username', $username) ->getQuery() ->getOneOrNullResult() ; } }
  48. class UserRepository extends ServiceEntityRepository { public function findOneByUsername(string $username): ?User

    { return $this->getEntityManager() ->createQuery('SELECT u FROM App\User u WHERE u.username = :username') ->setParameter('username', $username) ->getOneOrNullResult() ; } }
  49. None
  50. protected function getCatchablePatterns() { return [ '[a-z_][a-z0-9_]*\:[a-z_][a-z0-9_]*(?:\\\[a-z_][a-z0-9_]*)*', // aliased name

    '[a-z_\\\][a-z0-9_]*(?:\\\[a-z_][a-z0-9_]*)*', // identifier or qualified name '(?:[0-9]+(?:[\.][0-9]+)*)(?:e[+-]?[0-9]+)?', // numbers "'(?:[^']|'')*'", // quoted strings '\?[0-9]*|:[a-z_][a-z0-9_]*' // parameters ]; }
  51. None
  52. doctrine: orm: metadata_cache_driver: type: service id: doctrine.system_cache_provider query_cache_driver: type: service

    id: doctrine.system_cache_provider result_cache_driver: type: service id: doctrine.result_cache_provider
  53. None
  54. None
  55. $serializedString = sprintf( '%s:%d:"%s":0:{}', self::SERIALIZATION_FORMAT_AVOID_UNSERIALIZER, strlen($className), $className ); return unserialize($serializedString);

  56. use Doctrine\Instantiator\Instantiator; final class Dummy { private $foo; public function

    __construct(string $foo) { $this->foo = $foo; } public function getFoo(): string { return $this->foo; } } (new Instantiator())->instantiate(Dummy::class);
  57. class Dummy#4 (1) { private $foo => NULL } Uncaught

    TypeError: Return value of Dummy::getFoo() must be of the type string, null returned
  58. $ composer require stof/doctrine-extensions-bundle Using version ^1.3 for stof/doctrine-extensions-bundle ./composer.json

    has been updated Loading composer repositories with package information Updating dependencies (including require-dev) Restricting packages listed in "symfony/symfony" to "4.1.*" Prefetching 3 packages - Downloading (100%) Package operations: 3 installs, 0 updates, 0 removals - Installing behat/transliterator (v1.2.0): Loading from cache - Installing gedmo/doctrine-extensions (v2.4.36): Loading from cache - Installing stof/doctrine-extensions-bundle (v1.3.0): Loading from cache
  59. class User { // ... /** * @ORM\Column(type="datetime") * @Gedmo\Timestampable(on="create")

    */ private $createdAt; }
  60. final class Events { const preRemove = 'preRemove'; const postRemove

    = 'postRemove'; const prePersist = 'prePersist'; const postPersist = 'postPersist'; const preUpdate = 'preUpdate'; const postUpdate = 'postUpdate'; const postLoad = 'postLoad'; const loadClassMetadata = 'loadClassMetadata'; const onClassMetadataNotFound = 'onClassMetadataNotFound'; const preFlush = 'preFlush'; const onFlush = 'onFlush'; const postFlush = 'postFlush'; const onClear = 'onClear'; }
  61. None
  62. - Installing doctrine/lexer (v1.0.1): Loading from cache - Installing doctrine/annotations

    (v1.6.0): Loading from cache - Installing doctrine/event-manager (v1.0.0): Loading from cache - Installing doctrine/collections (v1.5.0): Loading from cache - Installing doctrine/cache (v1.8.0): Loading from cache - Installing doctrine/persistence (v1.0.1): Loading from cache - Installing doctrine/inflector (v1.3.0): Loading from cache - Installing doctrine/common (v2.9.0): Loading from cache - Installing doctrine/instantiator (1.1.0): Loading from cache - Installing doctrine/dbal (v2.8.0): Loading from cache - Installing doctrine/migrations (v1.8.1): Loading from cache - Installing doctrine/orm (v2.6.2): Loading from cache
  63. - Installing doctrine/lexer (v1.0.1): Loading from cache - Installing doctrine/annotations

    (v1.6.0): Loading from cache - Installing doctrine/event-manager (v1.0.0): Loading from cache - Installing doctrine/collections (v1.5.0): Loading from cache - Installing doctrine/cache (v1.8.0): Loading from cache - Installing doctrine/persistence (v1.0.1): Loading from cache - Installing doctrine/inflector (v1.3.0): Loading from cache - Installing doctrine/common (v2.9.0): Loading from cache - Installing doctrine/instantiator (1.1.0): Loading from cache - Installing doctrine/dbal (v2.8.0): Loading from cache - Installing doctrine/migrations (v1.8.1): Loading from cache - Installing doctrine/orm (v2.6.2): Loading from cache
  64. $ composer require doctrine/coding-standard Using version ^5.0 for doctrine/coding-standard ./composer.json

    has been updated Loading composer repositories with package information Updating dependencies (including require-dev) Restricting packages listed in "symfony/symfony" to "4.1.*"
  65. None
  66. /** * @author alcaeus * @since 1.0 */ final class

    Email { private $email; /** * @param string $email * @return void */ public function setEmail($email) { $this->email = $email; } /** * @return string|null */ public function getEmail() { return $this->email; } }
  67. $ vendor/bin/phpcbf src/Email.php PHPCBF RESULT SUMMARY ---------------------------------------------------------------------- FILE FIXED REMAINING

    ---------------------------------------------------------------------- src/Email.php 7 1 ---------------------------------------------------------------------- A TOTAL OF 7 ERRORS WERE FIXED IN 1 FILE ---------------------------------------------------------------------- Time: 203ms; Memory: 10Mb $ vendor/bin/phpcs src/Email.php FILE: src/Email.php ------------------------------------------------------------------------ FOUND 1 ERROR AFFECTING 1 LINE ------------------------------------------------------------------------ 9 | ERROR | Property \App\Email::$email does not have @var annotation. ------------------------------------------------------------------------ Time: 124ms; Memory: 8Mb
  68. final class Email { private $email; public function setEmail(string $email)

    : void { $this->email = $email; } public function getEmail() : ?string { return $this->email; } }
  69. <ruleset xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="vendor/squizlabs/php_codesniffer/phpcs.xsd"> <rule ref="Doctrine"/> <!-- Require no space around

    colon in return types --> <rule ref="SlevomatCodingStandard.TypeHints.ReturnTypeHintSpacing"> <properties> <property name="spacesCountBeforeColon" value="0"/> </properties> </rule> <!-- ... --> </ruleset>
  70. None
  71. None
  72. None
  73. None
  74. None
  75. None
  76. None
  77. None
  78. None
  79. None
  80. None
  81. None
  82. Thanks! @alcaeus github.com/alcaeus @doctrineproject https://joind.in/talk/2876c