Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Doctrine - more than an ORM

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.

alcaeus

October 26, 2018
Tweet

More Decks by alcaeus

Other Decks in Programming

Transcript

  1. 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' )); } }
  2. $ 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
  3. $ 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]: >
  4. class Email { private $id; private $email; public function __construct(string

    $email) { $this->email = $email; } public function getEmail(): string { return $this->email; } }
  5. /** * @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); } }
  6. 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 {}
  7. /** * @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; }
  8. 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; }
  9. use App\Annotation\SuperDuperClass; use Doctrine\ORM\Mapping as ORM; /** * @SuperDuperClass(level=5) *

    @ORM\Entity(repositoryClass=EmailRepository::class) */ class Email
  10. 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; }
  11. /** * @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); } }
  12. /** * 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;
  13. $ ./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
  14. 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'); } }
  15. $ ./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
  16. 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)%'
  17. 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"; }
  18. - 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
  19. - 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
  20. /** * @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); }
  21. /** * @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; }
  22. interface Collection extends Countable, IteratorAggregate, ArrayAccess { public function add($element);

    public function clear(); public function contains($element); public function isEmpty(); public function remove($key); // ... }
  23. 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; } ); }
  24. class UserRepository extends ServiceEntityRepository { public function findOneByUsername(string $username): ?User

    { return $this->createQueryBuilder('u') ->andWhere('u.username = :username') ->setParameter('username', $username) ->getQuery() ->getOneOrNullResult() ; } }
  25. 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() ; } }
  26. 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 ]; }
  27. 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
  28. 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);
  29. class Dummy#4 (1) { private $foo => NULL } Uncaught

    TypeError: Return value of Dummy::getFoo() must be of the type string, null returned
  30. $ 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
  31. 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'; }
  32. - 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
  33. - 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
  34. $ 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.*"
  35. /** * @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; } }
  36. $ 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
  37. final class Email { private $email; public function setEmail(string $email)

    : void { $this->email = $email; } public function getEmail() : ?string { return $this->email; } }
  38. <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>