├── LICENSE ├── README.md ├── git-hooks └── pre-commit.sh └── php-cs-fixer-config.php /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Davide Borsatto 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | This document contains a list of best practices and a style guide for development with PHP, Symfony and Doctrine. 2 | 3 | ## Style guide 4 | 5 | ### General 6 | 7 | #### Files 8 | 9 | Files for all languages (PHP, Javascript, Twig, YAML, XML, etc) must end with an empty line (the line feed character, LF). 10 | 11 | #### Line length 12 | 13 | Try to remain below 120 characters. Longer lines should be broken up, with the only exceptions being strings, which can go above the limit without requiring breaking up. 14 | 15 | In the event of function parameters being split into multiple lines, each parameter must be placed in a single line: 16 | 17 | ```php 18 | // Yes 19 | $service->action( 20 | $parameter1, 21 | $parameter2, 22 | $parameter3, 23 | $parameter4 24 | ); 25 | 26 | // No 27 | $service->action($parameter1, 28 | $parameter2, $parameter3, $parameter4); 29 | ``` 30 | 31 | There should not be "preemptive" line breaks. If some content can fit in a line, there should be no real reason to introduce a line break which would cause an extra level of indentation. 32 | 33 | #### Type declarations and strict typing 34 | 35 | All new code should declare in and out parameters as type declarations. All new files should have `declare(strict_types=1);` added at the beginning. 36 | 37 | ### Naming 38 | 39 | #### Classes 40 | 41 | In a Symfony project, it makes sense to use the same conventions as the Symfony code and use the prefix `Abstract` and the `Interface` and `Exception` suffix. 42 | 43 | ```text 44 | DateProviderInterface 45 | InvalidDateException 46 | AbstractDateProvider 47 | ``` 48 | 49 | While sometimes it may seem redundant, it is always preferable to employ a clear convention rather than discussing on a case-by-case scenario. 50 | 51 | #### Variables 52 | 53 | Variable names must not be too long or too short. Although there is no hard limit either way, it is common sense to not go below 3 characters long. 54 | 55 | #### Casing 56 | 57 | Variables and methods should be named using camel case, whereas classes and class-like structures (traits, interfaces, etc) should use pascal case. 58 | 59 | ### PHPDoc 60 | 61 | From PHP 7.0 onwards, it is possible to define in and out parameter types without the need to use PHPDoc. For this reason, the use of PHPDoc when no further information is added is considered redundant and to be avoided. 62 | 63 | When further information _can_ be added using PHPDoc, _all_ information should then be added because a small duplication is preferable to having two partial sources of data which then need to be combined by the reader. These are the main situations when PHPDoc use is required. 64 | 65 | #### Documenting exceptions 66 | 67 | If within a function an exception is thrown (both directly with `throw`, or inside another function which is called), it is _mandatory_ to add a `@throws` annotation in the PHPDoc block. The only exception to this rule is when having to work with objects like `DateTimeImmutable` which technically can throw an exception during construction, but only when the given string is invalid and often we can be reasonable sure that this will not be the case. In all other scenarios, exceptions must be documented. 68 | 69 | #### Documenting arrays 70 | 71 | In order for static analysis tools to work best with a codebase, all arrays should be documented. PHP arrays can actually represent multiple things, so it's important to always try to convey their real meaning. 72 | 73 | This is an example of how common structures should be documented. Further documentation can be found on the [Psalm website](https://psalm.dev/docs/annotating_code/type_syntax/array_types/). 74 | 75 | ```php 76 | /** @var list */ 77 | $array = [1, 2]; 78 | /** @var list */ 79 | $array = ['foo', 'bar']; 80 | /** @var list */ 81 | $array = [new stdClass(), new stdClass()]; 82 | 83 | /** @var array $array */ 84 | $array = [ 85 | 'foo' => 1, 86 | 'bar' => 2, 87 | ]; 88 | /** @var array $array */ 89 | $array = [ 90 | 'foo' => new stdClass(), 91 | 'bar' => new stdClass(), 92 | ]; 93 | /** @var array $array */ 94 | $array = [ 95 | 5 => 50, 96 | 10 => 100, 97 | ]; 98 | /** @var array> $array */ 99 | $array = [ 100 | 5 => [50], 101 | 10 => [100, 200], 102 | ]; 103 | /** @var array> $array */ 104 | $array = [ 105 | 5 => ['foo' => 'foo'], 106 | 10 => ['bar' => 'bar', 'baz' => 'baz'], 107 | ]; 108 | ``` 109 | 110 | Important: even though there is a way of documenting heterogeneous arrays, it is recommended to avoid using them and instead prefer the creation of ad-hoc value objects. 111 | 112 | ### Misc 113 | 114 | #### Importing classes and other elements 115 | 116 | For the sake of consistency (and a slight performance benefit), all elements that can be `use`d should be `use`d. This means that all classes (including those in the global namespace), all functions and all constants should be added to the `use` list at the beginning of a file. This can be easily done automatically using PHP-CS-Fixer, and avoids cluttering the code with backslashes for no real reason. All uses in the rest of the code must then refer to the imported symbol, without any namespace reference added to it. 117 | 118 | #### Fluent interfaces 119 | 120 | The rule of thumb with fluent interfaces is to have one _action_ per line. It's considered bad practice to use fluent interfaces within control structures and function calls. 121 | 122 | ```php 123 | // Yes 124 | $this->service->getStartDate() 125 | ->format('Y-m-d'); 126 | 127 | // No 128 | $this 129 | ->service 130 | ->getStartDate() 131 | ->format('Y-m-d'); 132 | 133 | // No 134 | $this->service->getStartDate()->format('Y-m-d'); 135 | ``` 136 | 137 | Accessing a property with `$this->property` does not count as an _action_, so as it's shown in the first example, having `$this->service->getStartDate()` in one line is valid. 138 | 139 | Exception: when function parameters must be broken up in separate lines, it may be helpful to not have an action in the first line in order to better align the following calls: 140 | 141 | ```php 142 | $builder 143 | ->add('startDate', DateType::class, [ 144 | 'required' => true, 145 | ]) 146 | ->add('endDate', DateType::class, [ 147 | 'required' => true, 148 | ]); 149 | ``` 150 | 151 | When breaking up a fluent interface in multiple lines, the basic unit of indentation (4 spaces) must be used. Indentation that changes according to the length of the name of a variable is considered brittle and should be avoided. 152 | 153 | ```php 154 | // Yes: renaming any variable will not affect other lines 155 | $queryBuilder = $entityManager->createQueryBuilder() 156 | ->from(MyEntity::class, 'e'); 157 | 158 | // No: renaming the entity manager or query builder variable 159 | // will cause all following lines to change indentation 160 | $queryBuilder = $entityManager->createQueryBuilder() 161 | ->from(MyEntity::class, 'e'); 162 | ``` 163 | 164 | #### Constant visibility 165 | 166 | Just like class properties, constants must have their visibility modifier declared. Also just like properties, private is always preferrable to public. 167 | 168 | #### Multiline ternary conditions 169 | 170 | Whenever a ternary operator requires to be broken up into multiple lines, the recommended way of doing this is the following: 171 | 172 | ```php 173 | $value = $condition 174 | ? 'value if true' 175 | : 'value if false'; 176 | ``` 177 | 178 | This way every result is clearly visible depending on which symbol the line starts with, with `?` at the beginning of the `true` path, and `:` at the beginning of the `false` path. 179 | 180 | ## Best practices 181 | 182 | ### Generic 183 | 184 | #### Accessing a property from within a class 185 | 186 | A property should be accessed directly, and using a getter for this should be considered an anti-pattern: 187 | 188 | ```php 189 | // No 190 | $this->getAuthor()->getName(); 191 | 192 | // Yes 193 | $this->author->getName(); 194 | ``` 195 | 196 | #### Variable assignment within control structures 197 | 198 | Except for a few specific cases, assignign values to a variable within a control structure (such as `if`, `while`, etc) should be considered a bad practice. 199 | 200 | ```php 201 | // Yes: 202 | $author = $blogPost->getAuthor(); 203 | if ($author) { 204 | 205 | // No: 206 | if ($author = $blogPost->getAuthor()) { 207 | ``` 208 | 209 | #### Domain and application exceptions 210 | 211 | Whenever a public method must throw an exception, this must be either a domain or application-level exception and never a PHP built-in one. Domain exceptions should extend an application-specific `DomainException` and not from others like `RuntimeException` or `InvalidArgumentException`, because a domain exception's goal should be to communicate a message specific to the domain, and categorizing it using a specific PHP built-in exception does not have any further advantage. 212 | 213 | Not all exceptions are domain exceptions: according to the separations in 3 levels (domain, application, infrastructure) of a "ports and adapters" architecture (also known as "hexagonal" architecture), some exceptions can be raised from contexts that are not domain-related. For this reason there should be a base `ApplicationException` that serves the same purpose of `DomainException`, but it's used within application boundaries. 214 | 215 | The infrastructure layer does not have a base exception because infrastructure components are integrated using domain or application ports (interfaces), and these interfaces must define themselves which exception can be thrown, therefore making these exceptions part of the domain layer. 216 | 217 | [This article](https://medium.com/@davide.borsatto/not-just-for-exceptional-circumstances-7692f2775a5a) discusses further the use of exceptions. 218 | 219 | #### Named constructors for exceptions 220 | 221 | While not mandatory, it is strongly recommended to use named constructors for creating exceptions: 222 | 223 | ```php 224 | class BlogPostException extends DomainException 225 | { 226 | public static function titleTooShort(string $title): self 227 | { 228 | return new self(sprintf('The title "%s" is too short for a blog post', $title)); 229 | } 230 | } 231 | ``` 232 | 233 | This type of use makes exception messages more consistent, and has the side effect of making error messages easily accessible in unit test contexts, so it's possible to test that a function has thrown *exactly* the expected exception. 234 | 235 | #### Use of DateTimeImmutable 236 | 237 | Wherever possibile, it is preferable to use `DateTimeImmutable` in place of `DateTime`. This is especially true for in and our parameters of functions, as the immutability guarantees that the value will not be changed by the function itself. 238 | 239 | #### Difference between Value Object and Data Transfer Object 240 | 241 | Within the context of a Symfony application it is useful to agree on these definitions: 242 | 243 | - A _value object_ is an immutable object that once it is built, is in a valid state. These objects require all mandatory parameters to be passed using the constructor, and provide no nullable parameters unless the nullability is explicitely defined by domain rules. 244 | If more ways to create a value object are found, these ways can be expressed using different named constructors. It's considered good practice not to mix usage of regular constructor and named constructors: should the second ones be used, it is recommended to define the actual constructor as private. 245 | Value objects can define getters to access properties, but no setters should be available. They can have methods that provide some sort of computed data using the internal state. 246 | As they are simple containers of data, they shoud not have access to objects that can be classified as services. The only parameters that can be passed to the constructors are those need to determine the internal state of the value object, but they can't be used to provide data computation functionality. 247 | According to DDD guidelines, a value object represents a standalone domain concept. Two value objects are considered the _same_ (regardless of PHP identity) if all of their properties have the same value. This is unlike entities, which are supposed to have some sort of identifier that determines the identity. 248 | - A _data transfer object_ is an object that can be created in an invalid state, and provides ways (methods or public properties) to change its internal state. It can be used as an object where data can be "accumulated" from different sources, and maybe as an object that is used as an intermediate state between forms and entities, as Symfony forms require that all object properties be nullable. 249 | 250 | Because of their immutability, value objects are usually preferable to data transfer objects. The suffix _VO_ is to be avoided, as the object should represent a domain concept, whereas the suffix _DTO_ can be used as they represent technical implementations. 251 | 252 | #### Always prefer structured, immutable data to unstructured arrays 253 | 254 | The use of arrays to transfer data from two different objects if strongly discouraged, except for when they are simple ordered lists of elements (so their type can be defined as `list`). The reason for this is that can represent heterogeneous and hard to document values, even when using proper type annotations. For example, a type `array` says that keys are strings, but it does not convey what these strings are and therefore it needs further documentation. Using these structures within a class is not a problem as the context is completely available in the same place, but they are to be avoided for communication between different classes as encapsulation is broken due to the receiver having to know implementation details in order to use the value. For these reasons, uses like these are considered bad practices: 255 | 256 | ```php 257 | class MyService 258 | { 259 | public function getCredentials(): array 260 | { 261 | return [$username, $password]; 262 | } 263 | 264 | public function getData(): array 265 | { 266 | // ... 267 | return [ 268 | 'name' => $name, 269 | 'age' => $age, 270 | // ... 271 | ]; 272 | } 273 | } 274 | ``` 275 | 276 | Situations like this warrant the creation of specific objects, preferably immutable, which have the side effect of using less memory as PHP can better optimize their usage. 277 | 278 | #### Avoid using abstract classes as type declarations 279 | 280 | An abstract class does not represent a promise, but rather a partial implementation. For this reason, it is semantically incorrect to use it as type declaration, as a type declaration defines the promise of a behavior (which should be defined by an interface) or a very specific implementation (which uses an actual class). Using an abstract class is a wrong way of categorizing objects that share some behavior, and for this reason interfaces should be used instead. 281 | 282 | #### FQCN usage 283 | 284 | The form `MyClass::class` should be used whenever possible instead of having to manually type the FQCN of a class. This use enables proper code analysis and removed the inconvenience of having to refer to classes using strings, which are inherently brittle and would be difficult to refactor. This syntas should also be used in entity annotations to refer to other classes (like related entities). 285 | 286 | ```php 287 | // Yes 288 | use App\Code\MyClass; 289 | 290 | public function getClassName(): string 291 | { 292 | return MyClass::class; 293 | } 294 | 295 | // No 296 | public function getClassName(): string 297 | { 298 | return 'App\Code\MyClass'; 299 | } 300 | ``` 301 | 302 | #### Object to string conversion 303 | 304 | While PHP natively offers the magic method `__toString()`, which is called automagically whenever an object is converted to a string, this approach relies on implicit behavior which [can be tricky to debug](https://github.com/ShittySoft/symfony-live-berlin-2018-doctrine-tutorial/pull/3) and hard to analyze (there is no easy way to find where some code is casting an object to string). 305 | 306 | For this reason, relying on the `__toString()` method is considered a bad practice, and an explicit `toString()` method should be used instead. For scenarios where the `__toString()` method is required by some specific use, both methods must be implemented and `__toString()` must forward to the real implementation. 307 | 308 | ```php 309 | public function __toString(): string 310 | { 311 | return $this->toString(); 312 | } 313 | 314 | public function toString(): string 315 | { 316 | return '...'; 317 | } 318 | ``` 319 | 320 | ### Symfony 321 | 322 | # Validation using POST_SUBMIT events in forms 323 | 324 | Form validations happen during the `POST_SUBMIT` even, using priority `0`. If an event listener is defined for this even, it must be remembered that according to the priority, the form may or may not have been validated. 325 | 326 | # Forcing rendering of collection form fields in Twig 327 | 328 | If in a Twig template a collection form field is rendered using a `for` loop, it must be taken into account that if the collection is empty, Twig will not handle the state properly and will not consider that field to have been rendered. For this reason, it will be wrongly rendered whenever `form_rest` is called. 329 | 330 | To avoid this, a manual call to `setRendered` should be placed as safeguard: 331 | 332 | ```twig 333 | {% for field in form.fields %} 334 | // 335 | {% else %} 336 | {% do form.fields.setRendered() %} 337 | {% endfor %} 338 | ``` 339 | 340 | #### Service injection and service locators 341 | 342 | Service retrieval using `$container->get($serviceId)` should be considered deprecated and must be avoided, with proper dependency injection using the constructor to be used instead. For the same reason, accessing repositories using `EntityManagerInterface::getRepository($entityName)` is also considered deprecated. 343 | 344 | #### Event dispatchers and subscribers 345 | 346 | Every event subscriber should subscribe to a single event. The method that handles the event should be called `handle`. 347 | 348 | #### In EntityType forms, prefer choices option over queryBuilder 349 | 350 | Creating a form that extends `EntityType`, choices can be set using two options: `choices`, which accepts a parameter of type `list`, or `queryBuilder`, which accepts either a query builder or a callback that must return the query builder. 351 | 352 | Things being equal, `choices` should be the preferred option, as the form type can correctly declare its dependency towards the repository, and the repository itself can be configured to return entities and not query builder objects, a behavior which violate the repository pattern. 353 | 354 | #### In EntityType forms, configure choice_label as callback 355 | 356 | Using the option `choice_label` it is possible to define how Symfony will convert an object into a string (using `__toString()` as fallback). When a custom method must be used, there are two approaches: 357 | 358 | ```php 359 | 'choice_label' => 'customMethod', 360 | // 361 | 'choice_label' => function (MyEntity $entity): string { 362 | return $entity->customMethod(); 363 | }, 364 | ``` 365 | 366 | The second option, despite longer, is preferable as the method will no longer be called "magically" by Symfony, with the code will be easier to analyze and the uses of the `customMethod` easier to track within the project. 367 | 368 | #### Collections 369 | 370 | Handling with collections must be seen as an implementation details within the entities, and ideally they should not be exposed by the public API. This has several advantages: 371 | - There is no doubt in what type will be returned (which will always be `list`) 372 | - Type hinting can be used without having to resort to the double annotation `Collection|list` 373 | - A collection can not be updated from outside the entity 374 | - Manually handling `setX` methods, we avoid possibile bugs due to lazy loading or overwriting of whole collections 375 | 376 | This is a complete example of a correct way of defining methods that work on a collection: 377 | 378 | ```php 379 | use Doctrine\Common\Collections\ArrayCollection; 380 | use Doctrine\Common\Collections\Collection; 381 | class Author 382 | { 383 | /** 384 | * @var Collection|list 385 | * 386 | * @ORM\OneToMany( 387 | * targetEntity=BlogPost::class, 388 | * mappedBy="author", 389 | * cascade={"persist"}, 390 | * orphanRemoval=true 391 | * ) 392 | */ 393 | private $blogPosts; 394 | 395 | public function __construct() 396 | { 397 | $this->blogPosts = new ArrayCollection(); 398 | } 399 | 400 | /** 401 | * @return list 402 | */ 403 | public function getBlogPosts(): array 404 | { 405 | return $this->blogPosts->toArray(); 406 | } 407 | 408 | public function addBlogPost(BlogPost $blogPost): void 409 | { 410 | if (!$this->blogPosts->contains($blogPost)) { 411 | $this->blogPosts->add($blogPost); 412 | } 413 | } 414 | 415 | public function removeBlogPost(BlogPost $blogPost): void 416 | { 417 | $this->blogPosts->removeElement($blogPost); 418 | } 419 | 420 | /** 421 | * @param list $blogPosts 422 | */ 423 | public function setBlogPosts(array $blogPosts): void 424 | { 425 | $this->blogPosts->clear(); 426 | foreach ($blogPosts as $blogPost) { 427 | $this->blogPosts->add($blogPost); 428 | } 429 | } 430 | } 431 | ``` 432 | In the property definition, the interface `Collection` should be used instead of the `ArrayCollection` implementation, which is used to initialize the value. The reason for this is that during execution, Doctrine will overwrite the property using a different `Collection` implementation, which enables lazy loading and other features. 433 | 434 | #### Repository definitions 435 | 436 | If Doctrine repositories need to extend a base service (which should not be the case), they must be defined by extending `Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository`, so there is no need to manually configure the repository and it can be used with autowiring. 437 | 438 | #### Repository return values 439 | 440 | The repository pattern defines that only entities or simple computation results (like an int for a count operation, or a boolean) should be returned. Returning a query builder object is an infrastructural leak, whereas converting entities into value objects implies that the repository is aware of implementation details of the specific use. 441 | 442 | #### Magic "find" methods in repositories 443 | 444 | Doctrine provides magic methods in its base repository services, which can be used as shortcut. The use of these methods, however, should be seen as bad practice, and the definition of custom and explicit methods should be preferable. 445 | -------------------------------------------------------------------------------- /git-hooks/pre-commit.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | git diff --name-only --cached --diff-filter=A HEAD | grep php | while read filename; do 4 | php bin/php-cs-fixer fix $filename --rules=declare_strict_types 5 | done 6 | 7 | git diff --name-only --cached --diff-filter=ACMRTUXB HEAD | grep php | while read filename; do 8 | php bin/php-cs-fixer fix $filename 9 | done 10 | -------------------------------------------------------------------------------- /php-cs-fixer-config.php: -------------------------------------------------------------------------------- 1 | in([__DIR__ . '/src']) 7 | ->name('*.php'); 8 | 9 | $config = new PhpCsFixer\Config(); 10 | 11 | return PhpCsFixer\Config::create() 12 | ->setFinder($finder) 13 | ->setRiskyAllowed(true) 14 | ->setRules([ 15 | '@PSR12' => true, 16 | '@DoctrineAnnotation' => true, 17 | '@PhpCsFixer' => true, 18 | '@Symfony' => true, 19 | 'concat_space' => [ 20 | 'spacing' => 'one', 21 | ], 22 | 'declare_strict_types' => true, 23 | 'general_phpdoc_annotation_remove' => true, 24 | 'global_namespace_import' => [ 25 | 'import_classes' => true, 26 | 'import_constants' => true, 27 | 'import_functions' => true, 28 | ], 29 | 'increment_style' => ['style' => 'post'], 30 | 'is_null' => true, 31 | 'list_syntax' => true, 32 | 'multiline_whitespace_before_semicolons' => [ 33 | 'strategy' => 'no_multi_line', 34 | ], 35 | 'native_constant_invocation' => true, 36 | 'native_function_invocation' => true, 37 | 'no_unused_imports' => true, 38 | 'no_superfluous_phpdoc_tags' => false, 39 | 'ordered_class_elements' => [ 40 | 'order' => [ 41 | 'use_trait', 42 | 'constant_private', 43 | 'constant_protected', 44 | 'constant_public', 45 | 'property_private', 46 | 'property_protected', 47 | 'property_public', 48 | 'construct', 49 | 'destruct', 50 | 'magic', 51 | ], 52 | 'sort_algorithm' => 'none', 53 | ], 54 | 'ordered_imports' => [ 55 | 'imports_order' => ['class', 'function', 'const'], 56 | 'sort_algorithm' => 'alpha', 57 | ], 58 | 'phpdoc_add_missing_param_annotation' => true, 59 | 'phpdoc_line_span' => [ 60 | 'property' => 'multi', 61 | 'method' => 'multi', 62 | ], 63 | 'php_unit_internal_class' => false, 64 | 'php_unit_test_class_requires_covers' => false, 65 | 'yoda_style' => [ 66 | 'equal' => false, 67 | 'identical' => false, 68 | 'less_and_greater' => false, 69 | ], 70 | ]); 71 | --------------------------------------------------------------------------------