├── .gitignore ├── package.json └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | package-lock.json 3 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "markdown-toc": "^1.2.0" 4 | }, 5 | "scripts": { 6 | "update-toc": "markdown-toc -i --maxdepth 3 README.md" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Paysera PHP style guide 2 | ===== 3 | 4 | Paysera PHP style guide extends [PSR-1](https://www.php-fig.org/psr/psr-1/) and [PSR-12](https://www.php-fig.org/psr/psr-12/), 5 | so code must also follow these standards to be compatible with this style guide. 6 | 7 | Guide is divided into several parts: 8 | - [PHP Basics](#php-basics). This defines additional style rules for the code, some restrictions for basic PHP usage; 9 | - [Main patterns](#main-patterns). This describes patterns and architecture decisions that must be followed; 10 | - [Symfony related conventions](#symfony-related-conventions). As Paysera use Symfony as the main framework, 11 | this chapter describes rules to be used when developing for this framework; 12 | - [Composer conventions](#composer-conventions). Describes usage rules related with composer: 13 | releasing libraries or requiring ones. 14 | 15 | # Table of contents 16 | 17 | 18 | 19 | - [PHP Basics](#php-basics) 20 | * [Basics](#basics) 21 | + [PHP code-level](#php-code-level) 22 | + [Globals](#globals) 23 | + [Files](#files) 24 | + [Exceptions for code style usage](#exceptions-for-code-style-usage) 25 | * [Code style](#code-style) 26 | + [Commas in arrays](#commas-in-arrays) 27 | + [Splitting in several lines](#splitting-in-several-lines) 28 | + [Chained method calls](#chained-method-calls) 29 | + [Constructors](#constructors) 30 | + [Variable, class and function naming](#variable-class-and-function-naming) 31 | + [Order of methods](#order-of-methods) 32 | + [Directories and namespaces](#directories-and-namespaces) 33 | + [Comparison order](#comparison-order) 34 | + [Namespaces and use statements](#namespaces-and-use-statements) 35 | * [Usage of PHP features](#usage-of-php-features) 36 | + [Condition results](#condition-results) 37 | + [Logical operators](#logical-operators) 38 | + [Strict comparison operators](#strict-comparison-operators) 39 | + [Converting to boolean](#converting-to-boolean) 40 | + [Comparing to boolean](#comparing-to-boolean) 41 | + [Comparing to `null`](#comparing-to-null) 42 | + [Assignments in conditions](#assignments-in-conditions) 43 | + [Unnecessary variables](#unnecessary-variables) 44 | + [Reusing variables](#reusing-variables) 45 | + [Unnecessary structures](#unnecessary-structures) 46 | + [Static methods](#static-methods) 47 | + [Converting to string](#converting-to-string) 48 | + [Double quotes](#double-quotes) 49 | + [Visibility](#visibility) 50 | + [Functions](#functions) 51 | + [`str_replace`](#str_replace) 52 | + [Return and argument types](#return-and-argument-types) 53 | + [Type-hinting classes and interfaces](#type-hinting-classes-and-interfaces) 54 | + [Dates](#dates) 55 | + [Exceptions](#exceptions) 56 | + [Checking things explicitly](#checking-things-explicitly) 57 | + [Calling parent constructor](#calling-parent-constructor) 58 | + [Traits](#traits) 59 | + [Arrays](#arrays) 60 | + [Default property values](#default-property-values) 61 | + [Scalar typehints](#scalar-typehints) 62 | + [Void typehints](#void-typehints) 63 | * [Comments](#comments) 64 | + [PhpDoc on methods](#phpdoc-on-methods) 65 | + [PhpDoc on properties](#phpdoc-on-properties) 66 | + [Fluid interface](#fluid-interface) 67 | + [Multiple available types](#multiple-available-types) 68 | + [Deprecations](#deprecations) 69 | + [Additional comments](#additional-comments) 70 | + [Comment styles](#comment-styles) 71 | * [IDE Warnings](#ide-warnings) 72 | - [Main patterns](#main-patterns) 73 | * [Thin model](#thin-model) 74 | * [Services without run-time state](#services-without-run-time-state) 75 | * [Composition over inheritance](#composition-over-inheritance) 76 | * [Services (objects) over classes, configuration over run-time parameters](#services-objects-over-classes-configuration-over-run-time-parameters) 77 | + [Constant usage for rarely changing configuration](#constant-usage-for-rarely-changing-configuration) 78 | * [Small, understandable methods](#small-understandable-methods) 79 | * [Dependencies](#dependencies) 80 | + [No unnecessary dependencies](#no-unnecessary-dependencies) 81 | + [No circular dependencies](#no-circular-dependencies) 82 | * [Services](#services) 83 | + [Service creation](#service-creation) 84 | + [Changing entity state](#changing-entity-state) 85 | + [Data types](#data-types) 86 | + [One-to-many Relation in Services](#one-to-many-relation-in-services) 87 | + [Event dispatcher](#event-dispatcher) 88 | * [Handling sensitive values](#handling-sensitive-values) 89 | + [Logging](#logging) 90 | + [Passing sensitive values as arguments](#passing-sensitive-values-as-arguments) 91 | + [Storing sensitive values in entities](#storing-sensitive-values-in-entities) 92 | - [Symfony related conventions](#symfony-related-conventions) 93 | * [Configuration](#configuration) 94 | + [Configuration inside bundles](#configuration-inside-bundles) 95 | + [Configuration in `app` directory](#configuration-in-app-directory) 96 | + [Parameters dist files](#parameters-dist-files) 97 | + [Files](#files-1) 98 | + [Naming](#naming) 99 | + [Services](#services-1) 100 | + [Routing](#routing) 101 | + [Production configuration](#production-configuration) 102 | + [Always full service IDs](#always-full-service-ids) 103 | * [Repositories](#repositories) 104 | + [Injection](#injection) 105 | + [Configuration](#configuration-1) 106 | + [Finding by ID](#finding-by-id) 107 | + [Queries](#queries) 108 | + [Return types](#return-types-1) 109 | + [Method naming](#method-naming-1) 110 | + [Only custom methods from outside of repository](#only-custom-methods-from-outside-of-repository) 111 | + [Repositories from other bundles](#repositories-from-other-bundles) 112 | + [Prefetching and filtering at the same time](#prefetching-and-filtering-at-the-same-time) 113 | + [Searching / paginating by date](#searching--paginating-by-date) 114 | * [Entities](#entities) 115 | + [Setters](#setters) 116 | + [Setters vs adders](#setters-vs-adders) 117 | + [Relations in setters](#relations-in-setters) 118 | + [States, types etc.](#states-types-etc) 119 | + [ID](#id) 120 | + [Creation date](#creation-date) 121 | + [Updated at](#updated-at) 122 | * [Doctrine](#doctrine) 123 | + [Flush](#flush) 124 | + [Database types and definitions](#database-types-and-definitions) 125 | + [Database naming](#database-naming) 126 | + [Class-map](#class-map) 127 | + [Extendability pattern instead of class-map](#extendability-pattern-instead-of-class-map) 128 | + [Saving Money instances](#saving-money-instances) 129 | + [Doctrine migrations](#doctrine-migrations) 130 | + [ID column strategy](#id-column-strategy) 131 | + [Variable types for query parameters](#variable-types-for-query-parameters) 132 | * [Controllers](#controllers) 133 | + [As services](#as-services) 134 | + [Static templates](#static-templates) 135 | + [Parameters passed to view](#parameters-passed-to-view) 136 | + [Small controllers](#small-controllers) 137 | + [Class naming](#class-naming-1) 138 | * [Events](#events) 139 | + [Available events](#available-events) 140 | + [Event naming](#event-naming) 141 | + [Listener naming](#listener-naming) 142 | + [Event class naming](#event-class-naming) 143 | * [Directory structure](#directory-structure) 144 | + [Root bundle directory](#root-bundle-directory) 145 | + [Basic directories for code](#basic-directories-for-code) 146 | + [Integration with other bundles](#integration-with-other-bundles) 147 | + [Special directories](#special-directories) 148 | + [Bundle namespace](#bundle-namespace) 149 | + [Example](#example) 150 | * [Twig](#twig) 151 | + [Calling methods](#calling-methods) 152 | * [Commands](#commands) 153 | + [Naming](#naming-1) 154 | + [Commands as services](#commands-as-services) 155 | * [Symfony version and new projects](#symfony-version-and-new-projects) 156 | + [Version and structure](#version-and-structure) 157 | + [Configuration](#configuration-2) 158 | * [REST in PHP](#rest-in-php) 159 | + [REST controllers](#rest-controllers) 160 | + [Result providers](#result-providers) 161 | + [Extending model](#extending-model) 162 | + [Permissions](#permissions) 163 | + [Pagination](#pagination) 164 | * [Migrating from legacy code](#migrating-from-legacy-code) 165 | * [New features](#new-features) 166 | * [Managing dependencies](#managing-dependencies) 167 | + [No hardcoded dependencies on legacy code](#no-hardcoded-dependencies-on-legacy-code) 168 | + [Interfaces](#interfaces) 169 | + [Configuration](#configuration-3) 170 | + [Example](#example-1) 171 | * [Database migrations](#database-migrations) 172 | - [Composer Conventions](#composer-conventions) 173 | * [Semantic versioning](#semantic-versioning) 174 | * [Changelog](#changelog) 175 | * [Releases](#releases) 176 | * [Reviewing tagging policy](#reviewing-tagging-policy) 177 | * [Initial library development](#initial-library-development) 178 | * [Constraints on library versions](#constraints-on-library-versions) 179 | * [Constraints on vendors](#constraints-on-vendors) 180 | * [Library version incrementing](#library-version-incrementing) 181 | + [Why?](#why) 182 | 183 | 184 | 185 | # PHP Basics 186 | 187 | ## Basics 188 | 189 | ### PHP code-level 190 | 191 | For our libraries we use lowest PHP version that is used in any of currently actively maintained projects. 192 | 193 | ### Globals 194 | 195 | We do not use global variables, constants and functions. 196 | 197 | ### Files 198 | 199 | We put every class to it’s own file. 200 | 201 | ### Exceptions for code style usage 202 | 203 | If we modify legacy code where some other conventions are used, we can use the same style as it is used there. 204 | 205 | ## Code style 206 | 207 | ### Commas in arrays 208 | 209 | If we split array items into separate lines, each item comes in it’s own line. 210 | Comma must be after each (including last) element. 211 | 212 | ```php 213 | **Why?** 223 | > 224 | > - each element in it's own line allows to easily scan all the elements - more understanding, less bugs; 225 | > - comma after last element allows to easily reorder or add new elements to the end of the array. 226 | 227 | ### Splitting in several lines 228 | 229 | If we split statement into separate lines, we use these rules: 230 | 231 | - `&&`, `||` etc. comes in the beginning of the line, not the end; 232 | 233 | - if we split some condition into separate lines, every part comes in it’s separate line 234 | (including first - on the new line, and last - new line after). All of the parts are indented. 235 | 236 | Some examples: 237 | 238 | ```php 239 | createQueryBuilder('a') 327 | ->join('a.items', 'i') 328 | ->andWhere('i.param = :param') 329 | ->setParameter('param', $param) 330 | ->getQuery() 331 | ->getResult() 332 | ; // semicolon here 333 | ``` 334 | 335 | > **Why?** 336 | > Same as with arrays: allows to easily scan all the calls, reorder or add new ones when needed. 337 | 338 | ### Constructors 339 | 340 | We always add `()` when constructing class, even if constructor takes no arguments: 341 | 342 | ```php 343 | **Why?** If we have base class witch implements the interface, we would have name clash. 382 | > For example, `ContainerAware` and `ContainerAwareInterface`. 383 | 384 | 385 | #### Property naming 386 | 387 | We use nouns or adjectives for property names, not verbs or questions. 388 | 389 | ```php 390 | **Why no ordering by visibility?** `protected` and `private` methods usually are used from single place in the code, 459 | > so ordering by functionality (versus by visibility) makes understanding the class easier. 460 | > If we just want to see class public interface, we can use IDE features 461 | > (method names ordered by visibility or/and name, including or excluding inherited methods etc.) 462 | 463 | 464 | ### Directories and namespaces 465 | 466 | #### Singular namespaces 467 | 468 | We use singular for namespaces: `Service`, `Bundle`, `Entity`, `Controller` etc. 469 | 470 | Exception: if English word does not have singular form. 471 | 472 | #### No `*Interface` namespaces 473 | 474 | We do not make directories just for interfaces, we put them together with services by related functionality 475 | (no `ServiceInterface` namespace). 476 | 477 | #### Different namespaces and service names 478 | 479 | We do not use service names for namespaces, instead we use abstractions. 480 | For example, namespace should be `UserMerge` or `UserMerging`, not `UserMergeManager`. 481 | 482 | ### Comparison order 483 | 484 | If we compare to static value (`true`, `false`, `null`, hard-coded integer or string), 485 | we put static value in the right of comparison operator. 486 | 487 | Wrong: `null === $something` 488 | 489 | Right: `$something === null` 490 | 491 | > **Why?** Speak clearly to be understood correctly, you should. Yeesssssss. 492 | 493 | ### Namespaces and use statements 494 | 495 | If class has a namespace, we use `use` statements instead of providing full namespace. 496 | This applies to PhpDoc comments, too. 497 | 498 | Wrong: 499 | 500 | ```php 501 | **Why?** `and` and `or` have different priorities and commonly leads to unintended consequences 555 | 556 | ### Strict comparison operators 557 | 558 | We use `===` (`!==`) instead of `==` (`!=`) everywhere. If we want to compare without checking the type, we should 559 | cast both of the arguments (for example to a string) and compare with `===`. 560 | 561 | Same applies for `in_array` - we always pass third argument as `true` for strict checking. 562 | 563 | We convert or validate types when data is entering the system - on normalizers, forms or controllers. 564 | 565 | > **Why?** Due to security reasons and to avoid bugs 566 | 567 | 568 | ```php 569 | php > var_dump(md5('240610708') == md5('QNKCDZO')); 570 | bool(true) 571 | php > file_put_contents('/tmp/a', 'abc'); 572 | php > $a = simplexml_load_file('/tmp/a'); 573 | php> var_dump($a == false); 574 | bool(true) 575 | php > var_dump(in_array(1, ['1a2b'])); 576 | bool(true) 577 | php > var_dump(in_array(true, ['a'])); 578 | bool(true) 579 | ``` 580 | 581 | ### Converting to boolean 582 | 583 | We use type casting operator to convert between types: 584 | 585 | ```php 586 | This should not be used at all. If variable is not `boolean` already, check it’s available values explicitly. 591 | 592 | 593 | ### Comparing to boolean 594 | 595 | We do not use `true`/`false` keywords when checking variable which is already `boolean`. 596 | 597 | Wrong: 598 | 599 | ```php 600 | getSomething() : null; 631 | } 632 | 633 | //or in php8 634 | function func(?ValueClass $value = null): ?string 635 | { 636 | return $value?->getSomething(); 637 | } 638 | ``` 639 | 640 | We also do not use `is_null` function for comparing. 641 | 642 | ### Assignments in conditions 643 | 644 | We do not use assignments inside conditional statements. 645 | 646 | Exception: in a `while` loop condition. 647 | 648 | Wrong: 649 | 650 | ```php 651 | get()) !== null && ($c = $b->get()) !== null) { 653 | $c->do(); 654 | } 655 | if ($project = $this->findProject()) { 656 | 657 | } 658 | ``` 659 | 660 | Correct: 661 | 662 | ```php 663 | get(); 665 | if ($b !== null) { 666 | $c = $b->get(); 667 | if ($c !== null) { 668 | $c->do(); 669 | } 670 | } 671 | 672 | $project = $this->findProject(); 673 | if ($project !== null) { 674 | 675 | } 676 | ``` 677 | 678 | > **Why?** We save a few lines of code but code is less understandable - we make several actions at once. 679 | > Furthermore, as we explicitly compare to `null`, conditional-assignment statements become complicated. 680 | 681 | 682 | ### Unnecessary variables 683 | 684 | We avoid unnecessary variables. 685 | 686 | Wrong: 687 | 688 | ```php 689 | get('documentId'); 741 | $document = $this->repository->find($document); // Illegal: we change variable's type 742 | // ... 743 | } 744 | 745 | public function thisIsCorrect(string $numberText, string $text, Request $request): void 746 | { 747 | $number = (int)$numberText; 748 | $modifiedText = $text . ' '; 749 | $documentId = $request->get('documentId'); 750 | $document = $this->repository->find($documentId); 751 | // ... 752 | } 753 | ``` 754 | 755 | > **Why?** To understand code better (quicker, easier) and thus to avoid bugs. This also integrated with IDE better 756 | > so it can give better static analysis support 757 | 758 | ### Unnecessary structures 759 | 760 | We avoid unnecessary structures. 761 | 762 | Wrong: 763 | 764 | ```php 765 | **Why?** We cannot throw exceptions in this method, we cannot put it in interface, 792 | > we cannot change return type or give any arguments - refactoring is impossible (only by changing it to simple method). 793 | > Also if debugging uses \_\_toString method, we cannot change it to get any additional information. 794 | 795 | 796 | ### Double quotes 797 | 798 | We do not use double quotes in simple strings. 799 | 800 | We use double quotes only under these conditions: 801 | 802 | - Single quote is used repeatedly inside the string 803 | - Some special symbols are used, like `"\n"` 804 | 805 | If we use double quotes, we do not use auto variable includes (`"Hello $name!"` or `"Hello {$object->name}!"`). 806 | 807 | ### Visibility 808 | 809 | #### Public properties 810 | 811 | We don’t use public properties. We avoid magic methods and dynamic properties - all used properties must be defined. 812 | 813 | Example: 814 | 815 | ```php 816 | alpha = $alpha; 827 | } 828 | } 829 | ``` 830 | 831 | #### Method visibility 832 | 833 | We prefer `private` over `protected` over `public` as it constraints the scope - it's easier to refactor, find usages, 834 | plan possible changes in code. Also IDE can warn about unused methods or properties. 835 | 836 | We use `protected` when we intend some property or method to be overwritten if necessary in extending classes. 837 | 838 | We use `public` visibility only when method is called from outside of class. 839 | 840 | ### Functions 841 | 842 | #### `count` 843 | 844 | We use `count` instead of `sizeof`. 845 | 846 | #### `is_null` 847 | 848 | We use compare to `null` using `===` instead of `is_null` function. For example: 849 | 850 | ```php 851 | if ($result === null) { 852 | // ... 853 | ``` 854 | 855 | ### `str_replace` 856 | 857 | We do not use `str_replace` if we need to remove prefix or suffix - this can lead to replacing more 858 | content unintentionally. 859 | 860 | ```php 861 | setValue($someValue); 941 | $entity->setValue(null); 942 | // but we do not use $entity->setValue(); 943 | ``` 944 | 945 | #### Void result 946 | 947 | We always return something or return nothing. If method does not return anything ("returns" `void`), 948 | we do not return `null`, `false` or any other value in that case. 949 | 950 | If method must return some value, we always specify what to return, even when returning `null`. 951 | 952 | Wrong: 953 | 954 | ```php 955 | has()) { 965 | return; 966 | } 967 | return $object->get(); 968 | } 969 | /** 970 | * @param int $requestId 971 | * @return bool 972 | * @throws PaybackUnavailableException 973 | */ 974 | function payback(int $requestId): bool 975 | { 976 | makePayback($requestId); 977 | return true; 978 | } 979 | ``` 980 | 981 | Correct: 982 | 983 | ```php 984 | has()) { 995 | return null; 996 | } 997 | return $object->get(); 998 | } 999 | /** 1000 | * @param int $requestId 1001 | * @throws PaybackUnavailableException 1002 | */ 1003 | function payback(int $requestId): void 1004 | { 1005 | makePayback($requestId); 1006 | } 1007 | ``` 1008 | 1009 | > **Why?** If method result is not used and you return something anyway, other programmer may assert that return value 1010 | > is indeed taken into account. For example, return `false` on failure, even if this failure is not handled anyhow by 1011 | > the functionality that's calling your method. 1012 | 1013 | #### Correct typehinting for possibly uninitialized properties 1014 | 1015 | If we declare some class property type as not nullable, after that class object construction 1016 | it should never be or become null. 1017 | 1018 | To rephrase from other perspective – if property is not initialized in the constructor, it's type must be nullable. 1019 | 1020 | This also applies for typehints in PhpDoc of properties. 1021 | 1022 | Examples: 1023 | ```php 1024 | this must include `|null`, as `$id` can be uninitialized 1032 | */ 1033 | private $id; 1034 | 1035 | public function getId(): ?int // return value here must be nullable 1036 | { 1037 | return $this->id; 1038 | } 1039 | 1040 | /** 1041 | * @return int|null 1042 | */ 1043 | public function getIdBefore71() 1044 | { 1045 | return $this->id; 1046 | } 1047 | 1048 | public function setId(int $id): self // we can use non-nullable type for setter, though 1049 | { 1050 | $this->id = $id; 1051 | 1052 | return $this; 1053 | } 1054 | } 1055 | ``` 1056 | 1057 | ```php 1058 | child === null) { 1069 | throw new RuntimeException('child is not initialized yet'); 1070 | } 1071 | 1072 | return $this->child; 1073 | } 1074 | 1075 | public function hasChild(): bool 1076 | { 1077 | return $this->child !== null; 1078 | } 1079 | } 1080 | ``` 1081 | 1082 | 1083 | ### Type-hinting classes and interfaces 1084 | 1085 | We always type-hint narrowest possible interface which we use inside the function or class. 1086 | 1087 | > **Why?** This allows us to refactor easier. We can just provide another class, which implements the same interface. 1088 | > We can also find real usages quicker if we want to change the interface itself 1089 | > (for example `RouterInterface` vs `UrlGeneratorInterface` when we change declaration of `RouterInterface::matches`). 1090 | 1091 | 1092 | #### Dependencies with several interfaces 1093 | 1094 | If we have dependency on service with several responsibilities (which implements several interfaces), 1095 | we should inject it twice. For example: 1096 | 1097 | ```php 1098 | declare(strict_types=1); 1099 | 1100 | class ResultNormalizer implements NormalizerInterface, DenormalizerInterface 1101 | { 1102 | // ... 1103 | } 1104 | 1105 | class MyService 1106 | { 1107 | public function __construct( 1108 | NormalizerInterface $normalizer, 1109 | DenormalizerInterface $denormalizer 1110 | ) { 1111 | // ... 1112 | } 1113 | // ... 1114 | } 1115 | 1116 | $resultNormalizer = new ResultNormalizer(); 1117 | $myService = new MyService($resultNormalizer, $resultNormalizer); 1118 | ``` 1119 | 1120 | ### Dates 1121 | 1122 | We use `\DateTimeImmutable` objects to represent date or date and time inside system. 1123 | 1124 | We use `\DateTimeInterface` where we get the date to be more compatible with functionality that still operates 1125 | `\DateTime` objects. 1126 | 1127 | > **Why?** Because immutable version is preventing accidental data modification by design. 1128 | 1129 | ### Exceptions 1130 | 1131 | #### Throwing 1132 | 1133 | We never throw base `\Exception` class except if we don’t intend for it to be caught. 1134 | 1135 | #### Catching 1136 | 1137 | We never catch base `\Exception` class except where it’s thrown from vendor code. 1138 | 1139 | In any case, we never catch exception if there are few throwing places possible and we only expect one of them. 1140 | 1141 | Wrong: 1142 | 1143 | ```php 1144 | service->someDeepMethod(); 1150 | // more code here 1151 | } catch (\Exception $exception) { 1152 | return null; // not found 1153 | } 1154 | ``` 1155 | 1156 | > **Why?** Because usually in these situations tens or hundreds different situations could have gone wrong but 1157 | > we make assumption that it was that single one. This is how we not only make the system behave incorrect on 1158 | > unexpected failures, but also ignore the real problems by not logging them. 1159 | 1160 | ### Checking things explicitly 1161 | 1162 | We use only functions or conditions that are designed for specific task we are trying to accomplish. 1163 | We don’t use unrelated features, even if they give required result with less code. 1164 | 1165 | We avoid side-effects even if in current situation they are practically impossible. 1166 | 1167 | Some examples: 1168 | - we use `isset` versus `empty` if we only want to check if array element is defined; 1169 | 1170 | - we use `$x !== ''` instead of `strlen($x) > 0` - length of `$x` has nothing to do with what we are trying 1171 | to check here, even if it gives us needed result; 1172 | 1173 | - we use `count($array) > 0` to check if array is not empty and not `!empty($array)`, 1174 | as we do not want to check whether `$array` is `0`, `false`, `''` or even not defined at all 1175 | (in which case IDE would possibly hide some warnings that could help noticing possible bugs). 1176 | 1177 | > **Why?** We avoid side-effects if some related code changes or is refactored. 1178 | > Code is much easier to understand if we see specific checks or function calls instead of something unrelated that 1179 | > happens to give the needed result. 1180 | 1181 | 1182 | ### Calling parent constructor 1183 | 1184 | If we need to call parent constructor, we do it as first statement in constructor. For example: 1185 | 1186 | ```php 1187 | public function __construct(int $arg1, int $arg2) 1188 | { 1189 | parent::__construct($arg1); 1190 | $this->setArg2($arg2); 1191 | } 1192 | ``` 1193 | 1194 | > **Why?** Parent class can have some mandatory stuff to do before we can use it's functionality. 1195 | > See following example. This is the only way to call parent constructor in some (probably most) 1196 | > of other languages (etc. JAVA) 1197 | 1198 | 1199 | Example of parent class, with which calling constructor later would fail: 1200 | 1201 | ```php 1202 | /** 1203 | * @var int[] $params 1204 | */ 1205 | private array $params; 1206 | public function __construct(int $arg1) 1207 | { 1208 | $this->params = [$arg1]; 1209 | } 1210 | public function setArg2(int $arg2) 1211 | { 1212 | $this->params[] = $arg2; 1213 | } 1214 | ``` 1215 | 1216 | ### Traits 1217 | 1218 | The only valid case for traits is in unit test classes. We avoid using traits in our base code. 1219 | 1220 | > **Why?** We use (classical) OOP to refactor functionality to avoid duplicated code. 1221 | 1222 | 1223 | ### Arrays 1224 | 1225 | We always use short array syntax (`[1, 2]` instead of `array(1, 2)`) where PHP code-level allows it. 1226 | 1227 | ### Default property values 1228 | 1229 | If we need to define some default value for class property, we do this in constructor, not in property declaration. 1230 | 1231 | > **Why?** Some default values cannot be set when declaring properties, so this way everything is in one place. 1232 | > It especially makes sense when entity has lots of properties. 1233 | 1234 | 1235 | ```php 1236 | one = new ArrayCollection(); 1254 | $this->two = self::TYPE_TWO; 1255 | $this->three = 3; 1256 | $this->four = false; 1257 | $this->createdAt = new DateTimeImmutable(); 1258 | } 1259 | } 1260 | ``` 1261 | 1262 | ### Scalar typehints 1263 | 1264 | #### Strict types declaration is required 1265 | 1266 | For newly written files we always add `declare(strict_types=1);` at the top of the file. 1267 | 1268 | We add it for already existing files if we can guarantee that no negative side-effects would be caused by it. 1269 | It's safer to type-cast variables in the file to the correct scalar types to keep the consistent behavior. 1270 | 1271 | #### Always use scalar typehints where appropriate 1272 | 1273 | We always use scalar typehints both for arguments and return types, unless our code with them would not work 1274 | as intended so we are forced not to use them. 1275 | 1276 | For already existing methods we can add scalar typehints but we're responsible to guarantee that any place 1277 | in the project that has strict types declaration and calls this method always passes the correct type as an argument. 1278 | We could add typecast in the places where we're not guaranteed to keep the consistent behavior. 1279 | 1280 | If we add scalar typehint for any argument in a public method in our library, this means that major release is needed. 1281 | Scalar typehints can be always safely added to return values if needed, though. 1282 | 1283 | ```php 1284 | **Why?** Any comment needs to be maintained – if we add parameter, change parameter or return type, we must also update the PhpDoc. If PhpDoc does not add any additional information, it just duplicates information that's already provided. 1366 | 1367 | #### Additional information in PhpDoc 1368 | 1369 | If there is additional information about method which is not available in method signature then only that information 1370 | will be added in PhpDoc as there is no need to duplicate information, eg. 1371 | 1372 | ```php 1373 | /** 1374 | * Gets a day limit starting from midnight. 1375 | * 1376 | * @see https://example.com/ 1377 | */ 1378 | public function getDayLimit(Client $client): Money; 1379 | ``` 1380 | 1381 | ```php 1382 | /** 1383 | * @param Transfer[] $transfers 1384 | * @throws TransferProcessorException 1385 | */ 1386 | public function processTransfers(array $transfers): void; 1387 | ``` 1388 | 1389 | ```php 1390 | /** 1391 | * @dataProvider dataProviderForSomeMethodTest 1392 | * @throws Exception 1393 | */ 1394 | public function testSomeMethod(int $input, string $expectation): void; 1395 | ``` 1396 | 1397 | #### PhpDoc on arrays 1398 | 1399 | When we take as an argument or return an array of strictly typed elements, we should add a PhpDoc to describe the 1400 | type of elements in the array. 1401 | 1402 | ```php 1403 | /** 1404 | * @param Client[] $clients 1405 | */ 1406 | public function getDayLimitForClients(array $clients): Money; 1407 | ``` 1408 | 1409 | ### PhpDoc on properties 1410 | 1411 | We use PhpDoc on properties that are not injected via constructor if PHP language level does support nullable strict types. 1412 | 1413 | We do *not* put PhpDoc on services, that are type-casted and injected via constructor, as they are automatically 1414 | recognised by IDE and desynchronization between typecast and PhpDoc can cause warnings to be silenced. 1415 | 1416 | ### Fluid interface 1417 | 1418 | If method returns `$this`, we use typehint `: self` that IDE could guess correct type if we use this method 1419 | for objects of subclasses. 1420 | 1421 | ### Multiple available types 1422 | 1423 | If we return or take as an argument value that has few types or can be one of few types, 1424 | we provide all of them separated by `|`. 1425 | 1426 | ```php 1427 | **Why?** This way we 1443 | > 1) know if `null` value can be returned or passed as an argument; 1444 | > 2) can use methods with auto-completion from both of specified types. 1445 | > This is very convenient for collections - we can use `$collection->count()` 1446 | > and `foreach ($collection as $a) {$a->someMethod();}` 1447 | 1448 | 1449 | ### Deprecations 1450 | 1451 | If method is deprecated, we mark this in PhpDoc as `@deprecated` with description or additional 1452 | `@see` with new method to use. For example: 1453 | 1454 | ```php 1455 | @deprecated use DI to create and inject this service 1456 | ``` 1457 | 1458 | ```php 1459 | @deprecated 1460 | @see \Acme\Component\SomeComponent::someMethod 1461 | ``` 1462 | 1463 | ### Additional comments 1464 | 1465 | We do not add file comments. 1466 | 1467 | > **Why?** We use class comments - they are used if PhpDoc is auto-generated from PHP code, also can be used by IDE. 1468 | 1469 | 1470 | We do not use `@package`, `@namespace`, `@date` or `@author` annotations anywhere. 1471 | 1472 | > **Why?** Namespace is provided by PHP keyword. Both date and author can be seen via annotations and becomes stale 1473 | > really quickly. After refactoring, the only name in the file can be totally irrelevant. 1474 | 1475 | We do not use `@inheritdoc`. 1476 | 1477 | > **Why?** It does not contain information for programmer. For IDE, no PhpDoc at all has the same results. 1478 | 1479 | 1480 | ### Comment styles 1481 | 1482 | We use multi-line `/** */` comments for method, property and class annotations. 1483 | 1484 | We use single-line `/** @var Class $object */` annotation for local variables. We always avoid this if possible - 1485 | usually PhpDoc is enough (sometimes it's missing, for example in vendor code). 1486 | 1487 | We can use `//` single line comments in the code. 1488 | 1489 | We do not use `/* */` or `#` comments at all. 1490 | 1491 | > **Why no multi-line comment?** We try to keep functions small and comment them in PhpDoc. 1492 | > If some things are to be explained in function body, single line comments should be enough. 1493 | > If we want to comment-out something, we just delete it (and use VCS to revert if needed) or disable functionality 1494 | > in configuration. 1495 | 1496 | Some examples: 1497 | ```php 1498 | processors as $processor) { 1510 | /** @var Mail $mail this must be single line - 1 line, not 3 */ 1511 | $mail = $processor->createNamedMail($newsletter->getName()); 1512 | //.. 1513 | } 1514 | 1515 | // we can add single line comments to explain behavior 1516 | } 1517 | } 1518 | ``` 1519 | 1520 | We always prefer refactoring code to be understandable over adding some comments to explain it's behavior. 1521 | Most of the cases, code should be clear enough to not need any comments at all. 1522 | 1523 | > **Why should we avoid comments?** They are technical debt - if you cannot keep it updated, it gets stale real quick. 1524 | > When you cannot trust some of the comments, you don't know which ones you can really trust. 1525 | 1526 | ## IDE Warnings 1527 | 1528 | We always avoid IDE warnings if possible (it's not possible only when there is an IDE bug or some similar case). 1529 | We provide `@var` PhpDoc comment if IDE cannot guess the type of variable and we cannot fix method declaration for 1530 | type to be correctly guessed (vendor code etc.) 1531 | 1532 | --------- 1533 | 1534 | 1535 | # Main patterns 1536 | 1537 | ## Thin model 1538 | 1539 | We use Plain Value Objects (we name them Entities) for moving information/data around functionality. 1540 | 1541 | We do not put any logic into those objects - all logic is handled outside, in services. 1542 | 1543 | This allows to change and configure behaviour in different context. 1544 | 1545 | ## Services without run-time state 1546 | 1547 | In general we try to make services without run-time state. 1548 | That is, once configured, it should not change it’s behaviour in run-time. 1549 | 1550 | This does not apply to specific services, which have, for example, to collect some events and redispatch them in the 1551 | end of request etc. - in those cases where collecting the state in run-time is their primary objective. 1552 | 1553 | Instead of: 1554 | 1555 | ```php 1556 | name = $name; 1563 | 1564 | return $this; 1565 | } 1566 | public function search(): void 1567 | { 1568 | // do something with $this->name 1569 | } 1570 | } 1571 | ``` 1572 | 1573 | we use: 1574 | 1575 | ```php 1576 | **Why?** When services become context-aware unintentionally, this makes unpredicted side-effects. 1587 | > More bugs, which can be very hard to debug and/or test. Much more test scenarios. Much harder to refactor. 1588 | 1589 | > This is similar to using globals - if we store run-time context, 1590 | > we become unaware of all the things that can influence the behavior. 1591 | 1592 | 1593 | ## Composition over inheritance 1594 | 1595 | We always try to use composition instead of inheritance. 1596 | 1597 | If we want to make some abstract method - we inject interface into constructor with that method. 1598 | 1599 | This helps to follow single responsibility principle. Also this often allows to configure object by injecting different 1600 | functionality without explosion of classes. If we have 2 abstract methods and 2 possible logic for each of them, 1601 | we would need 4 classes with 8 methods total, also having some duplicated code. 1602 | If we inject services, we only need 4 classes with 1 method each with no duplicated code. 1603 | 1604 | Also, this allows to test code much easier, as we can test each functionality separately. 1605 | 1606 | ## Services (objects) over classes, configuration over run-time parameters 1607 | 1608 | This is related to *composition over inheritance* - we try to configure each service instead of hard-coding 1609 | any of parameters in the code itself. 1610 | 1611 | For example, instead of: 1612 | 1613 | ```php 1614 | class ServiceAlpha 1615 | { 1616 | public function doSomething(): void 1617 | { 1618 | $alpha = Manager::getInstance(); 1619 | $alpha->doSomething('a'); 1620 | } 1621 | } 1622 | class ServiceBeta 1623 | { 1624 | public function doSomething(): void 1625 | { 1626 | $alpha = Manager::getInstance(); 1627 | $alpha->doSomething('b'); 1628 | } 1629 | } 1630 | $alpha = new ServiceAlpha(); 1631 | $beta = new ServiceBeta(); 1632 | ``` 1633 | 1634 | we use: 1635 | 1636 | ```php 1637 | class Service 1638 | { 1639 | private Manager $manager; 1640 | public function __construct(Manager $manager) 1641 | { 1642 | $this->manager = $manager; 1643 | } 1644 | public function doSomething(): void 1645 | { 1646 | $this->manager->doSomething(); 1647 | } 1648 | } 1649 | $alpha = new Service(new Manager('a')); // we use Dependency Injection Container for creating services, this is just an example 1650 | $beta = new Service(new Manager('b')); 1651 | ``` 1652 | 1653 | This allows to reuse already tested code in different scenarios just by reconfiguring it. 1654 | 1655 | Of course, we still need to test the configuration itself (functional/integration testing), as it gets more complicated. 1656 | 1657 | ### Constant usage for rarely changing configuration 1658 | 1659 | We follow the [Symfony best practice](https://symfony.com/doc/8.0/best_practices.html#use-constants-to-define-options-that-rarely-change) 1660 | of using constants for configuration options that rarely change. 1661 | 1662 | #### Why use constants? 1663 | 1664 | - **Type safety**: Constants are checked at compile time, while configuration parameters can have typos 1665 | - **IDE support**: Better autocompletion and refactoring capabilities 1666 | - **Reduced complexity**: No need to manage configuration files for static values 1667 | - **Co-location**: The value lives alongside the code that uses it 1668 | 1669 | #### When to use constants 1670 | 1671 | Use constants when: 1672 | - The value is truly static and doesn't change between environments 1673 | - The value is domain-specific and logically belongs to a class 1674 | - You don't need to override the value for testing 1675 | 1676 | ```php 1677 | // Good - constants for static configuration 1678 | class Post 1679 | { 1680 | public const NUMBER_OF_ITEMS_PER_PAGE = 10; 1681 | public const MAX_TITLE_LENGTH = 255; 1682 | } 1683 | 1684 | class TokenGenerator 1685 | { 1686 | private const TOKEN_LENGTH = 32; 1687 | private const TOKEN_ALPHABET = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'; 1688 | 1689 | public function generate(): string 1690 | { 1691 | // uses self::TOKEN_LENGTH and self::TOKEN_ALPHABET 1692 | } 1693 | } 1694 | ``` 1695 | 1696 | #### When to use DI container parameters 1697 | 1698 | Configuration parameters should be defined in DI container only when necessary: 1699 | 1700 | **1. Value depends on environment or is taken from environment variables:** 1701 | ```xml 1702 | 1703 | 1704 | %env(API_BASE_URL)% 1705 | 1706 | 1707 | ``` 1708 | 1709 | **2. Different configuration is required for testing:** 1710 | 1711 | When tests need to override a value (e.g., shorter timeouts, different thresholds), inject it as a constructor argument: 1712 | ```php 1713 | class TimeoutAwareClient 1714 | { 1715 | public function __construct(private readonly int $timeoutSeconds) 1716 | { 1717 | } 1718 | } 1719 | ``` 1720 | ```xml 1721 | 1722 | 1723 | 30 1724 | 1725 | 1726 | 1727 | 1728 | 1 1729 | 1730 | ``` 1731 | 1732 | **3. Value is shared across multiple service definitions:** 1733 | ```xml 1734 | 1735 | 20 1736 | 1737 | 1738 | 1739 | 1740 | %default_page_size% 1741 | 1742 | 1743 | 1744 | %default_page_size% 1745 | 1746 | 1747 | ``` 1748 | 1749 | #### What to avoid 1750 | 1751 | Do not declare constants inside the DI container: 1752 | 1753 | ```xml 1754 | 1755 | 1756 | 255 1757 | 1758 | 1759 | 1760 | 1761 | %post_max_title_length% 1762 | 1763 | 1764 | ``` 1765 | 1766 | ```php 1767 | // Good - use a constant directly 1768 | class PostValidator 1769 | { 1770 | public function validate(Post $post): void 1771 | { 1772 | if (strlen($post->getTitle()) > Post::MAX_TITLE_LENGTH) { 1773 | // ... 1774 | } 1775 | } 1776 | } 1777 | ``` 1778 | 1779 | Using constants reduces code complexity and provides better IDE support while still allowing constants to be 1780 | used in Symfony's service container when needed. 1781 | 1782 | ## Small, understandable methods 1783 | 1784 | We try to write code that is self-explanatory. 1785 | This implies writing small methods - we should understand what the method does by looking at it's code. 1786 | We also try to name methods by their meaning, which also helps understanding the code. 1787 | 1788 | ## Dependencies 1789 | 1790 | We always take into account component or bundle dependencies. 1791 | 1792 | ### No unnecessary dependencies 1793 | 1794 | No unnecessary dependencies between components or bundles must be created. 1795 | 1796 | If there is some abstract functionality, it must not depend upon it’s implementations. 1797 | 1798 | ### No circular dependencies 1799 | 1800 | No circular dependencies between components or bundles must be created. 1801 | 1802 | ## Services 1803 | 1804 | ### Service creation 1805 | 1806 | Services can be created only by factory classes or dependency injection container. 1807 | We do not create services in other services not dedicated to do that (for example, in constructor). 1808 | 1809 | ### Changing entity state 1810 | 1811 | #### Use of managers 1812 | 1813 | We make changes to entity state in managers or some similar designated services. 1814 | 1815 | Manager can make the following actions: 1816 | 1817 | - check initial entity state before changing it, validate it; 1818 | - change the state; 1819 | - dispatch event; 1820 | - make some additional actions. 1821 | 1822 | If state is changed in a controller or some other place, duplicated code can occur when functionality is 1823 | required in another place. With duplicated code, different behavior can occur, which eventually leads to bugs. 1824 | 1825 | #### Methods for changing state 1826 | 1827 | We prefer concrete methods instead of one abstract method to change state. 1828 | 1829 | This allows us to see available actions and make our code clearer: 1830 | no `switch` statements, event maps, complex validation rules, nested \`if\`s or similar magic. 1831 | 1832 | ```php 1833 | assertPending($request); 1843 | $request->setStatus(Request::STATUS_ACCEPTED); 1844 | $this->eventDispatcher->dispatch(RequestEvents::ACCEPTED, new RequestEvent($request)); 1845 | } 1846 | public function deny(Request $request): void 1847 | { 1848 | $this->assertPending($request); 1849 | $request->setStatus(Request::STATUS_DENY); 1850 | $this->eventDispatcher->dispatch(RequestEvents::DENIED, new RequestEvent($request)); 1851 | } 1852 | 1853 | // more complicated and totally unclear from interface 1854 | public function changeStatus(Request $request, string $status): void 1855 | { 1856 | $this->assertPending($request); 1857 | switch ($status) { 1858 | case Request::STATUS_ACCEPTED: 1859 | $eventName = RequestEvents::ACCEPTED; 1860 | break; 1861 | case Request::STATUS_DENY: 1862 | $eventName = RequestEvents::DENIED; 1863 | break; 1864 | default: 1865 | throw new \InvalidArgumentException('Invalid status provided ' . $status); 1866 | } 1867 | $request->setStatus($status); 1868 | $this->eventDispatcher->dispatch($eventName, new RequestEvent($request)); 1869 | } 1870 | } 1871 | ``` 1872 | 1873 | To make example more complicated, we can imagine that different validation groups must be provided for validator 1874 | for each of the given status and some other event class should be provided if request is denied. 1875 | Furthermore, denial could take one more argument - `DenialReason $reason`. 1876 | 1877 | ### Data types 1878 | 1879 | We use objects to represent data when possible. 1880 | For example, if data can be represented by both scalar value and an object (like date), we use object. 1881 | When several variables belong to single item, we use an object representing them and do not pass or operate on 1882 | these separately (like money object versus amount and currency as separate variables). 1883 | 1884 | We always try to use single representation of that data item - we avoid having multiple classes which represent the same 1885 | object in the system (for example Transfer as DTO in common library and as an Entity - these could both co-exist, 1886 | but we try to have an Entity as soon as possible in the system). 1887 | 1888 | We normalize data as soon as possible in the system (input level) and denormalize it as later as possible 1889 | (output level). This means that in service level, where our business logic resides, 1890 | we always operate on these objects and not their concrete representations. 1891 | 1892 | #### Identifier usage 1893 | 1894 | We operate with objects inside services, not their identifiers. 1895 | Objects must be resolved by their identifiers in controller, normalizer or similar input layer. 1896 | In business objects (entities or simple DTOs, like `Filter`) we already have other objects, taken from database. 1897 | 1898 | If we do not find object by identifier, we throw exception and return `404` as a response 1899 | (or specific error code with `400`). 1900 | 1901 | #### Date and time 1902 | 1903 | In some cases we use integer UNIX timestamp to represent date with time. 1904 | When creating `DateTimeImmutable`, we must not use constructor with `@` as it ignores the time zone of the server. 1905 | 1906 | For example: 1907 | 1908 | ```php 1909 | setTimestamp($data['created_at']); 1912 | $entity->setCreatedAt($createdAt); 1913 | ``` 1914 | 1915 | #### Money 1916 | 1917 | Always amount and currency, never only amount. 1918 | 1919 | ### One-to-many Relation in Services 1920 | 1921 | #### Structure 1922 | 1923 | If there is functionality that can be implemented in different ways and should be selected at runtime, 1924 | we create Interface for it and use Manager (or separate Registry class) to collect all those services that implement it. 1925 | 1926 | From code, we do not call those services directly, we call Manager which chooses the correct service and 1927 | redirects call to it. 1928 | 1929 | This way if Interface changes, we only have to make changes in the Manager. 1930 | Also, if we need to do some action every time some method is called, we can add it to manager, 1931 | no need to put it to all those services. 1932 | 1933 | Example: 1934 | 1935 | ```php 1936 | declare(strict_types=1); 1937 | 1938 | class Manager 1939 | { 1940 | /** 1941 | * @var ProviderInterface[] 1942 | */ 1943 | private $providers = []; 1944 | 1945 | public function addProvider(ProviderInterface $provider, string $providerKey): self 1946 | { 1947 | $this->providers[$providerKey] = $provider; 1948 | 1949 | return $this; 1950 | } 1951 | 1952 | public function doSomething(Data $data): string 1953 | { 1954 | if (!isset($this->providers[$data->getProviderKey()])) { 1955 | throw new RuntimeException(); 1956 | } 1957 | 1958 | return $this->providers[$data->getProviderKey()]->doSomething($data); 1959 | } 1960 | } 1961 | ``` 1962 | 1963 | #### Tags 1964 | 1965 | We use dependency injection container tags to add all those services to the manager. 1966 | 1967 | ### Event dispatcher 1968 | 1969 | We use events to notify about some result or to optionally change behaviour before making some action. 1970 | 1971 | We do not use events to actually make some action happen, which is mandatory for the place which dispatches the event. 1972 | 1973 | We should not make assumptions about listeners of some event. 1974 | If we require a result from event listeners, this indicates that we should refactor functionality and use interfaces 1975 | with tags or some similar solution. 1976 | 1977 | 1978 | ## Handling sensitive values 1979 | 1980 | Sensitive values are those that are secret and should not be known or read in plain text. 1981 | For example passwords, tokens, secret generated codes etc. Any values that can affect security of the system. 1982 | 1983 | ### Logging 1984 | 1985 | We never log content where sensitive values can be found. For example, request content for all requests, as these 1986 | include call to authentication. We must either 1987 | 1) do not log at all; 1988 | 2) make whitelist what to log; 1989 | 3) make blacklist when to avoid logging (this is not recommended as can be easily forgotten). 1990 | 1991 | When we configure loggers, we always use normalizers that does not extract private fields from any given objects 1992 | by default. 1993 | 1994 | ### Passing sensitive values as arguments 1995 | 1996 | As we log exception stack trace, any scalar values passed as arguments can be used in stack trace. 1997 | To avoid that, we put sensitive values to `SensitiveValue` object as soon as they enter our system. 1998 | We pass them as this object. We get the value itself only in lowest level possible. 1999 | 2000 | If we cannot pass `SensitiveValue` (for example, to vendor library), we catch `\Exception` and re-throw a new one, 2001 | without providing last exception from before (we can copy the message itself, if it does not contain sensitive values). 2002 | 2003 | ### Storing sensitive values in entities 2004 | 2005 | If we really need to store sensitive value in an entity, we make that property private. 2006 | We also make that setter and getter would operate with `SensitiveValue` and not scalar type itself. 2007 | 2008 | Better approach is to always use hash of sensitive values, if we just need to check if provided value matches it. 2009 | If we need to resend generated code or use original value later, only in that case we save generated value (like code) 2010 | in plain-text. 2011 | 2012 | 2013 | 2014 | 2015 | 2016 | 2017 | 2018 | # Symfony related conventions 2019 | 2020 | This chapter describes conventions related directly with Symfony framework and related components (Doctrine, Twig). 2021 | 2022 | It also includes some closely related conventions that can be used independently (for example in libraries). 2023 | 2024 | ## Configuration 2025 | 2026 | ### Configuration inside bundles 2027 | 2028 | We use XML configuration inside bundles. 2029 | 2030 | > **Why not YAML?** We use XML, because it is explicit and has schema definition - many mistakes can be avoided. 2031 | 2032 | > **Why not annotations?** We don’t use annotations, as we want to leave configuration and logic separate. 2033 | > Moreover, class and object/service are not the same, this makes it difficult to configure several services 2034 | > for a single class. 2035 | 2036 | ### Configuration in `app` directory 2037 | 2038 | We use YAML configuration inside `app` directory for semantic configuration files. 2039 | 2040 | > **Why?** Configuration is semantic here and thus not very complex. 2041 | > Most of the examples use only this format, it’s default for Symfony standard distribution. 2042 | 2043 | We still use XML for service definitions inside `app` directory. 2044 | 2045 | ### Parameters dist files 2046 | 2047 | #### File naming 2048 | 2049 | We use `{origName}.dist.{origExt}` name for distribution versions of parameters. 2050 | 2051 | > **Why?** Original extension is left the same so we can use all IDE features. 2052 | > It is configurable (or not important) so we **can** do that. 2053 | 2054 | #### Parameter naming 2055 | 2056 | We do not put bundle prefix to parameters that we only use outside the bundle, for example to configure 2057 | `config.yml` file. Treat them as private to that bundle. 2058 | 2059 | **Incorrect**: 2060 | 2061 | `config.yml`: 2062 | 2063 | ```yaml 2064 | acme_cool_feature: 2065 | parameter: %acme_cool_feature.parameter% 2066 | option: %acme_cool_feature.option% 2067 | ``` 2068 | 2069 | `parameters.dist.yml`: 2070 | 2071 | ```yaml 2072 | acme_cool_feature.parameter: A 2073 | acme_cool_feature.option: B 2074 | ``` 2075 | 2076 | **Correct**: 2077 | 2078 | `config.yml`: 2079 | 2080 | ```yaml 2081 | acme_cool_feature: 2082 | parameter: %cool_feature_parameter% 2083 | option: %cool_feature_option% 2084 | ``` 2085 | 2086 | `parameters.dist.yml`: 2087 | 2088 | ```yaml 2089 | cool_feature_parameter: A 2090 | cool_feature_option: B 2091 | ``` 2092 | 2093 | We can use some other naming, as long as it has no bundle prefix. 2094 | 2095 | **Why?** 2096 | 2097 | Because when using bundle prefix, we make assumptions about their usage inside the bundle itself. 2098 | It would brake or have unintended effects in a case like this: 2099 | 2100 | ```php 2101 | // ... 2102 | $definition->setArguments(array($config['parameter'], $config['option'])); 2103 | 2104 | // here we have a problem - our parameter from parameters.yml unintentionally overwrites this parameter in the bundle: 2105 | $container->setParameter('evp_cool_feature.parameter', 'ABC'); 2106 | // ... 2107 | ``` 2108 | 2109 | In other words, we have prefixes to avoid parameter clashing, by using prefix in `config.yml` 2110 | (or other bundles - that's the same), we can break things. 2111 | 2112 | #### Contents 2113 | 2114 | Default parameters (inside `parameters.dist.yml`) must be configured for development environment. 2115 | 2116 | > **Why?** We cannot use production as this would be security issue. 2117 | > All developers can add default values and change them only if needed. 2118 | 2119 | We use value `pass` for default passwords in development environment. 2120 | 2121 | We use default domains in parameters - if everywhere configured the same, no changes in parameters should be needed. 2122 | 2123 | Basic rule: using default parameters must always work for any developer out-of-the-box with default environment set-up. 2124 | 2125 | ### Files 2126 | 2127 | #### Imports 2128 | 2129 | When configuring services, we split definitions into separate files and import them in main 2130 | `services.xml` and `routing.xml` files. For example: 2131 | 2132 | ```txt 2133 | config/services/controllers.xml 2134 | config/services/forms.xml 2135 | config/services/repositories.xml 2136 | config/services/normalizers.xml 2137 | ``` 2138 | 2139 | We put routing prefix into `` directive. 2140 | 2141 | #### Directories 2142 | 2143 | We put configuration files in subdirectories - `services/`, `routing/` etc. 2144 | 2145 | ### Naming 2146 | 2147 | Service ID always begins with bundle identifier, like `acme_newsletter`, followed by dot. 2148 | 2149 | If type of service is repository, controller, listener or normalizer, these are following parts: 2150 | 2151 | - type of service, like `repository`, `controller`, `normalizer` 2152 | - name of the service, excluding the type. 2153 | 2154 | If service is a manager, registry or some other unique service for that bundle, we use it’s identifier directly without the type. 2155 | 2156 | Valid service names: `acme_page.page_manager`, `acme_page.repository.page` 2157 | 2158 | ### Services 2159 | 2160 | #### Class names 2161 | 2162 | We explicitly state class names for our services, we do not put them in separate parameters. 2163 | 2164 | Instead of this: 2165 | 2166 | ```xml 2167 | Namespace\ClassName 2168 | 2169 | 2170 | ``` 2171 | 2172 | we use this: 2173 | 2174 | ```xml 2175 | 2176 | ``` 2177 | 2178 | > **Why?** This makes unnecessary difficulty when understanding available services and their structure. 2179 | > If we need to overwrite service, we overwrite it by it's ID, so we could change not only the class name, 2180 | > but also constructor arguments or make additional method calls. 2181 | > If functionality is to be changed by circumstances, we should make ability to change this service 2182 | > using bundle semantic configuration. 2183 | 2184 | 2185 | #### Factory services 2186 | 2187 | We use new syntax for defining factory services: 2188 | 2189 | ```xml 2190 | 2191 | 2192 | 2193 | ``` 2194 | 2195 | #### ID as FQCN (class name) 2196 | 2197 | We prefer to use FQCN (App\Service\MyService) as service id over custom service id value. This helps to avoid 2198 | boilerplate code and allows easier configuration. Service definitions that use custom service id value 2199 | ("app.bundle.my_service") can still be used if it fits existing app configuration style in particular configuration file 2200 | or bundle / module. 2201 | 2202 | #### Autoconfiguration and autowiring 2203 | 2204 | We don't use autowiring and autoconfiguration features of Dependency Injection component. 2205 | 2206 | > **Why?** It might be quicker at the beginning to use it, but to understand the code, we would need to guess where 2207 | > the service really comes from. If we would need to refactor something, it might require to write DI configuration 2208 | > for already (automatically) defined services, too. 2209 | 2210 | ### Routing 2211 | 2212 | #### Route prefix 2213 | 2214 | We always use bundle name as route prefix, just like in services. We leave out vendor prefix. 2215 | 2216 | Exception: we drop out `-common` suffix, if one exists. Routes should not clash with corresponding 2217 | bundle without `-common`. 2218 | 2219 | #### Route naming 2220 | 2221 | We try to use names to identify the action taken by route, not methods to take it. 2222 | 2223 | For example, `create_page` instead of `page_post`. 2224 | 2225 | ### Production configuration 2226 | 2227 | We do not put specific configuration in bundles - we provide semantic configuration for those and define them inside 2228 | `app/config.yml`. 2229 | 2230 | We do not put secret parts of production configuration anywhere in repository - no private key files, common secret 2231 | strings, passwords etc. They must be ignored by git and provided in `parameters.yml`, shared directory or in 2232 | some other way. 2233 | 2234 | ### Always full service IDs 2235 | 2236 | We never dynamically build parameter names or service IDs. We use tags if we need abstraction. 2237 | We define map or state all things explicitly if needed. 2238 | 2239 | Do not do this: 2240 | 2241 | ```php 2242 | $container->get('my_prefix.' . $type . '.provider'); 2243 | ``` 2244 | 2245 | > **Why?** 2246 | > 1) It's hard to find all possible services which are registered (and thus hard to refactor - many potential pitfalls); 2247 | > 2) we do not control if service is even registered in container; 2248 | > 3) we cannot add some other configuration; 2249 | > 4) we do not test if service implements some specific interface; 2250 | > 5) we cannot get all possible types available or all possible services. 2251 | 2252 | Instead do this: 2253 | 2254 | ```php 2255 | public function addProvider(SomeInterface $provider, string $type): self 2256 | { 2257 | $this->providers[$type] = $provider; 2258 | 2259 | return $this; 2260 | } 2261 | 2262 | public function getProvider(string $type): ProviderInterface 2263 | { 2264 | if (!isset($this->providers[$type])) { 2265 | throw new RuntimeException(); 2266 | } 2267 | 2268 | return $this->providers[$type]; 2269 | } 2270 | 2271 | // ... 2272 | 2273 | public function build(ContainerBuilder $container): void 2274 | { 2275 | $container->addCompilerPass(new AddTaggedCompilerPass( 2276 | 'my_prefix.my_registry', 2277 | 'my_prefix.tag_name', 2278 | 'addProvider', 2279 | ['type'] 2280 | )); 2281 | } 2282 | 2283 | // ... 2284 | 2285 | $someRegistry->getProvider($type); 2286 | ``` 2287 | 2288 | And tag services: 2289 | 2290 | ```xml 2291 | 2292 | 2293 | 2294 | 2295 | ``` 2296 | 2297 | ## Repositories 2298 | 2299 | ### Injection 2300 | 2301 | We inject repository objects inside the services via constructor. 2302 | 2303 | Thus, we do not use `EntityManager::getRepository` in controllers or services. 2304 | 2305 | ### Configuration 2306 | 2307 | We configure repository classes with `lazy=true`. 2308 | 2309 | > **Why?** Creating repository class requires loading Entity metadata, which requires cache to be loaded at 2310 | > service construction 2311 | 2312 | ### Finding by ID 2313 | 2314 | For finding entity by ID we always use `find` method, not `findOneById` etc. 2315 | 2316 | Exception: If we want to preload some related entities. 2317 | 2318 | Another exception: If we override `findOneById` to contain call to `find`. 2319 | This could be used to provide PhpDoc for IDE to guess the type returned. 2320 | 2321 | > **Why?** `find` uses internal Doctrine cache, while simple queries to database (even only by ID) by default does not 2322 | 2323 | ### Queries 2324 | 2325 | We use query builder and queries inside repositories only - we don’t build queries inside controllers or other services. 2326 | 2327 | ### Return types 2328 | 2329 | #### Basic usage 2330 | 2331 | Repository methods starting with `find` returns instance(-s) of the class, that this repository is related to, 2332 | or scalar values. 2333 | 2334 | #### Advanced usage 2335 | 2336 | Let's take an example: 2337 | - we have `UserBundle`, which does not have any dependencies; 2338 | - we have `AvatarBundle`. It does depend on `UserBundle`, but reverse is not true (and shouldn't). 2339 | 2340 | We need some repository to return `User` entities, that does not have any avatars. 2341 | 2342 | We cannot put this to `AvatarRepository`, as it should return only `Avatar` entities. 2343 | We cannot put it anywhere in the `UserBundle` as it would make a hard (and unwanted) dependency on `AvatarBundle`. 2344 | 2345 | In this case we create a separate `UserRepository` service inside `AvatarBundle`, inject `EntityManager` manually 2346 | and make needed queries in there. 2347 | 2348 | ### Method naming 2349 | 2350 | We use `findOneBy*` to find one object and `findBy*` to find list of objects. 2351 | 2352 | We use `getQueryBuilderFor*` to get query builders (for example for pagination), 2353 | `findCountBy*` etc. to get scalar results. 2354 | 2355 | ### Only custom methods from outside of repository 2356 | 2357 | We do not use `findOneBy` or `findBy` methods from outside of repository. 2358 | We create method inside repository for that concrete use-case. In that method, we can call `$this->findBy(...)` 2359 | with given parameters. 2360 | 2361 | > **Why?** This allows to add more constraints in one single place. For example, after adding `deleted` column we can 2362 | > update all queries inside repository to filter those records out by default. 2363 | > Also we can see all possible queries in one place - this allows us to easily review which indexes should be 2364 | > defined and which are unused. 2365 | 2366 | ### Repositories from other bundles 2367 | 2368 | We avoid direct usage of repositories from other bundles than the repository belongs to. 2369 | 2370 | If we need to get information from another bundle, 2371 | we tend to use services (which, in that bundle, can inject that repository). 2372 | 2373 | > **Why?** This creates proxy service for getting needed information. 2374 | > Even if at first it would only pass method call to repository itself, when something changes we can refactor it 2375 | > easier - we can inject other services, handle filtering of arguments or results etc. 2376 | > This helps a lot if method in repository is used from many many places inside whole project and we need to change 2377 | > some logic with other injected service. 2378 | 2379 | ### Prefetching and filtering at the same time 2380 | 2381 | We do not select related entities if we filter by them. For example, do **not** do this: 2382 | 2383 | ```php 2384 | createQueryBuilder('user') 2386 | ->select('user, email') 2387 | ->leftJoin('user.emails', 'email') 2388 | ->where('email.status = :activeStatus') 2389 | ->setParameters([ 2390 | 'activeStatus' => User::STATUS_ACTIVE, 2391 | ]) 2392 | ->getQuery() 2393 | ->getResult() 2394 | ; 2395 | ``` 2396 | 2397 | If we iterate over user entities that were returned, and call `getEmails()` on them, we will **not** get all emails 2398 | for that user. We will get only those that matched the original query (active emails in this case). 2399 | Furthermore, it breaks code such as this later in the process: 2400 | 2401 | ```php 2402 | $user = $userRepository->find(123); 2403 | $user->getEmails(); // this will also return *only* active emails, as the user is cached in Doctrine's identity map 2404 | ``` 2405 | 2406 | What are the options? 2407 | 2408 | 1) change `->select('user, email')` to `->select('user')`, so we do not pre-fetch emails; 2409 | 2) pre-fetch them with a separate join: 2410 | Keep in mind that pre-fetching is OK and recommended in most of the cases where we will use those relations. 2411 | It's just not good if we also filter by that relation. 2412 | Further work and filtering should still be done within the code as results will contain users with at least one active email, but won't have users which all emails are not active - in other words we filter out users without active emails. 2413 | ```php 2414 | createQueryBuilder('user') 2416 | ->select('user, email') 2417 | ->leftJoin('user.emails', 'email') 2418 | ->leftJoin('user.emails', 'activeEmail') 2419 | ->where('activeEmail.status = :activeStatus') 2420 | ->setParameters([ 2421 | 'activeStatus' => User::STATUS_ACTIVE, 2422 | ]) 2423 | ->getQuery() 2424 | ->getResult() 2425 | ; 2426 | // --- 2427 | foreach ($users as $user) { 2428 | foreach ($user->getEmails() as $email) { 2429 | if ($email->getStatus() === User::STATUS_ACTIVE) { 2430 | // do something 2431 | } 2432 | } 2433 | } 2434 | ``` 2435 | 3) select only those fields which are relevant for you within that specific case: 2436 | Entities won't be resolved, plain array would be returned, hence Doctrine's identity map is not built. 2437 | ```php 2438 | $result = $this->createQueryBuilder('user') 2439 | ->select('user.name AS userName, email.email AS userEmail, email.status AS emailStatus') 2440 | ->leftJoin('user.emails', 'email') 2441 | ->where('email.status = :activeStatus') 2442 | ->setParameters([ 2443 | 'activeStatus' => User::STATUS_ACTIVE, 2444 | ]) 2445 | ->getQuery() 2446 | ->getResult() 2447 | ; 2448 | 2449 | $return = []; 2450 | foreach ($result as $item) { 2451 | $return[] = (new NormalizedReturnObject()) 2452 | ->setUserName($item['userName']) 2453 | ->setUserEmail($item['userEmail']) 2454 | ->setEmailStatus($item['emailStatus']) 2455 | ; 2456 | } 2457 | 2458 | return $return; 2459 | ``` 2460 | 2461 | 2462 | ### Searching / paginating by date 2463 | 2464 | When searching by date period, we take beginning inclusively and ending exclusively. 2465 | 2466 | > **Why?** We do not need to add or remove one second from time periods, code gets much more readable. 2467 | > If both would be inclusive or exclusive, when iterating through periods, some results would be provided in both 2468 | > periods and some results on no periods. 2469 | 2470 | On the other hand, in the user interface, if user wants to set dates inclusively, 2471 | frontend logic has to map between UI and backend/API convention (usually by adding a day to the end date). 2472 | 2473 | ## Entities 2474 | 2475 | ### Setters 2476 | 2477 | All setters in entity returns `$this` for fluent interface. 2478 | 2479 | ### Setters vs adders 2480 | 2481 | Setters always resets the previous value. 2482 | Adders leaves previous value and only adds provided to already existing collection. 2483 | 2484 | ### Relations in setters 2485 | 2486 | In many-to-one relation, `many` side (the owning one) always fixes relations. 2487 | This is done in the `add*` method, adding reverse relation to `$this`. 2488 | 2489 | In this case `set*` contains code to reset collection and `add*` every item in provided collection, so that 2490 | 1) every item in collection would have relation fixed; 2491 | 2) collection object would be the same as before `set*`. 2492 | 2493 | ```php 2494 | leaves = new ArrayCollection(); 2505 | } 2506 | 2507 | /** 2508 | * @param Leaf[] $leaves we do not typecast to array or Collection as this can be any iterable 2509 | * @return $this 2510 | */ 2511 | public function setLeaves(array $leaves): self 2512 | { 2513 | $this->leaves->clear(); 2514 | foreach ($leaves as $leaf) { 2515 | $this->addLeaf($leaf); 2516 | } 2517 | 2518 | return $this; 2519 | } 2520 | 2521 | public function addLeaf(Leaf $leaf): self 2522 | { 2523 | $this->leaves[] = $leaf; 2524 | $leaf->setTree($this); 2525 | 2526 | return $this; 2527 | } 2528 | } 2529 | ``` 2530 | 2531 | ### States, types etc. 2532 | 2533 | We save states, types and other choice-type information as strings in database. 2534 | 2535 | We provide constants with possible property values in Entity. 2536 | 2537 | Constant name starts with property name, followed by the value itself. 2538 | 2539 | ```php 2540 | type = self::TYPE_SIMPLE; 2554 | } 2555 | } 2556 | ``` 2557 | 2558 | ### ID 2559 | 2560 | We do not provide `setId` method as we shouldn't use it - we either persist new entity 2561 | or take it from repository and update it. 2562 | 2563 | ### Creation date 2564 | 2565 | We set `createdAt` property in constructor if we need creation date. 2566 | 2567 | > **Why not Doctrine extension?** This works faster and more reliable. 2568 | > Furthermore, even new not-yet-persisted entities has `createdAt` value, so we can assert that it’s always available. 2569 | > Also this makes unit testing easier as we do not need to mock extensions. 2570 | 2571 | ### Updated at 2572 | 2573 | We do not put `updatedAt` to entities unless needed. Valid use-case is if we need to see when **any** of the fields 2574 | was changed, for example if synchronizing relational database via cron job into some other storage etc. 2575 | 2576 | In any other use-case `updatedAt` does not give needed information. 2577 | Either it is not used at all, either it is used where it shouldn't. 2578 | 2579 | For example, if we need to see when some state has changed, we need to put extra column or even one-to-many relation 2580 | to be sure that the date saved really represents that state change, not some other change in any other of entity 2581 | fields. Even if there is one (besides `id` and `updatedAt`) field in entity, extra one can be added later and this 2582 | would break the functionality (or naming). 2583 | 2584 | ## Doctrine 2585 | 2586 | ### Flush 2587 | 2588 | #### Number of flushes 2589 | 2590 | We always avoid more than one `flush` in one web request. 2591 | 2592 | #### Where to flush 2593 | 2594 | We use `flush` only in controllers, commands and workers. 2595 | These are the top input layer to the system, and is used usually only once per request life-cycle. 2596 | Services (and especially listeners) can be used in many different ways, so we cannot make assumption that a few of 2597 | them will not get called in the same request. 2598 | 2599 | #### Multiple flushes 2600 | 2601 | Services that do flush the database or use other services that flush the database (in any depth) should not live in 2602 | namespace `Service/`, but in separate namespaces such as `Processor/`. 2603 | 2604 | ```php 2605 | entityManager = $entityManager; 2622 | } 2623 | 2624 | public function process(): void 2625 | { 2626 | $foo = new Foo('name'); 2627 | $createdFoo = $this->remoteServiceClient->createFoo($foo); 2628 | $foo->setRemoteId($createdFoo['id']); 2629 | $this->entityManager->flush(); 2630 | 2631 | $this->remoteServiceClient->confirmFoo($foo); 2632 | $foo->setStatus('done'); 2633 | $this->entityManager->flush(); 2634 | } 2635 | } 2636 | ``` 2637 | 2638 | ### Database types and definitions 2639 | 2640 | When we use Doctrine `string` type, we try to figure out sensible maximum length for each case separately, we 2641 | shouldn't just use `255` as default one. 2642 | 2643 | We should always validate or ensure in some other way that the length of the value fits to the maximum length 2644 | before storing it to the database. 2645 | 2646 | > **Why?** While maximum length does not affect size in disk, it can affect performance. When INTERNAL TEMPORARY 2647 | tables are created (for joins, derived tables, counts, group by, order by etc), it's created in RAM only if intermediate 2648 | results are small enough. This size is calculated taking maximum length of fields, and not actually used space. 2649 | Thus unnecessary bigger maximum lengths can cause more I/O on the server and slower query times. 2650 | 2651 | ### Database naming 2652 | 2653 | #### Underscored 2654 | 2655 | We use underscored names for both table names and column names. 2656 | 2657 | #### Plural 2658 | 2659 | We use plural nouns as table names. 2660 | 2661 | > **Why?** This makes SQL statements more natural language - we select some items from a collection. 2662 | 2663 | #### Prefix 2664 | 2665 | We add prefix to all databases, consisting of bundle name without vendor. 2666 | `AcmePageBundle:PageRoute` -> `page_page_routes`. 2667 | 2668 | #### SQL keywords 2669 | 2670 | We avoid SQL keywords in table and column names, but leave them as-is in PHP-side. 2671 | 2672 | ```php 2673 | 2685 | ``` 2686 | 2687 | ### Class-map 2688 | 2689 | We avoid Doctrine class-map and extending entities. 2690 | 2691 | > **Why?** 2692 | > 1) Extending comes with very bad performance (many relations are queried automatically even if they are 2693 | > never used afterwards), and many `instanceof` checks in the code; 2694 | > 2) this is not extendable, as base bundle must know about all others extending it’s entity; 2695 | > 3) we cannot change instance of Entity (change it's subclass) once it's created, which also makes things 2696 | > unnecessarily difficult. 2697 | 2698 | ### Extendability pattern instead of class-map 2699 | 2700 | We use such a pattern: 2701 | 2702 | - Base entity has only the information, which is common to all entities. 2703 | Thus this information is used inside base bundle in some services. 2704 | 2705 | - Base entity has field `type`, `providerKey` or similar. It defines, which bundle/manager/provider created 2706 | this entity and knows more information. 2707 | 2708 | - There is Manager or similar service in base bundle, which provides methods to manage this entity 2709 | (and it’s subclasses). This service has method `addProvider` or similar, accepting `ProviderInterface` (or similar) 2710 | 2711 | - There is a compiler pass, which adds Providers (etc., implementing interface from base bundle) 2712 | to Manager via `add\*` method. We can always use the same class from `lib-dependency-injection` repository, 2713 | no need to create separate for each case, only if there is some custom logic. 2714 | 2715 | - Providers live in their bundles, optionally having additional information in some separate entities, 2716 | which possibly have relation to the base entity. 2717 | 2718 | Example: 2719 | 2720 | - `PageBundle` has entity `Block`, which has `id` and `providerKey`. 2721 | 2722 | - `BlockManager` has method `renderBlock(Block $block)` and `addProvider(ProviderInterface $provider, $key)`. 2723 | It lives in `PageBundle`. Methods from `ProviderInterface` are used only in this manager, 2724 | (bad: `manager→getProvider(block)→render(block)`, good: `manager→render(block)`). 2725 | This allows to refactor some of the code more easily if needed. 2726 | 2727 | - There is compiler pass registered, which adds all tagged providers to manager via `addProvider` method. 2728 | 2729 | - `ProviderInterface` lives in `PageBundle` and has single method - `renderBlock(Block $block)`. 2730 | 2731 | - `TextBundle` has Entity `TextBlock`, which has fields `block` and `textId`. 2732 | 2733 | - `TextBlockProvider` implements `ProviderInterface` and lives in `TextBundle`. 2734 | `renderBlock` method searches for `TextBlock` related to the given block and renders text via `textId`. 2735 | 2736 | Such structure let’s extend services without reverse dependencies. 2737 | As base bundle has no dependencies, it can be extracted to library and used in other projects. 2738 | 2739 | Also the code is not changed in the base bundle - we can only test the new providers etc., 2740 | no need to test if we didn’t brake anything in the base bundle. 2741 | 2742 | ### Saving Money instances 2743 | 2744 | #### No getters for amount and currency 2745 | 2746 | We only provide getter and setter for `Money` object, not for it's fields 2747 | 2748 | #### No handling with events 2749 | 2750 | We convert to and from `Money` object only in getters and setters, we do not use eventing system for that. 2751 | It could lead to unsaved fields, as Doctrine does not watch object property, it only watches amount and currency 2752 | properties, so in some cases there can be no event at all. 2753 | Also this would make understanding structure, debugging and testing harder. 2754 | 2755 | #### We do not store Money objects themselves 2756 | 2757 | As money object is 1) immutable 2) never compared by reference, we do not store the object itself in Doctrine-persisted Entities. 2758 | We store only it's internal fields and recreate it when needed. This makes the code simpler. 2759 | 2760 | ```php 2761 | priceAmount = null; 2773 | $this->priceCurrency = null; 2774 | } 2775 | 2776 | public function setPrice(?Money $price = null): self 2777 | { 2778 | if ($price === null) { 2779 | $this->priceAmount = null; 2780 | $this->priceCurrency = null; 2781 | } else { 2782 | $this->priceAmount = $price->getAmount(); 2783 | $this->priceCurrency = $price->getCurrency(); 2784 | } 2785 | 2786 | return $this; 2787 | } 2788 | 2789 | public function getPrice(): ?Money 2790 | { 2791 | return $this->priceAmount !== null && $this->priceCurrency !== null 2792 | ? new Money($this->priceAmount, $this->priceCurrency) 2793 | : null; 2794 | } 2795 | } 2796 | ``` 2797 | 2798 | If Entity is not persisted by Doctrine, we just store Money object itself as usual. 2799 | 2800 | #### We store amount as decimal 2801 | 2802 | Inside Doctrine configuration: 2803 | 2804 | ```xml 2805 | 2806 | ``` 2807 | 2808 | This allows to sum and perform other number-based operations inside the database. 2809 | Also, it auto-validates amount to be numeric (just in case we didn't handle it), and it takes less space, 2810 | so also performs better if indexing. 2811 | 2812 | We store currency as a string. 2813 | 2814 | ### Doctrine migrations 2815 | 2816 | For migrating database structure we always use Doctrine migrations. 2817 | There should be no changes inside generated migration if we run `doctrine:migrations:diff` after 2818 | `doctrine:migrations:migrate`. 2819 | 2820 | We do not put custom `ALTER` or `CREATE` statements inside migration scripts ourselves - we modify `xml` file and let 2821 | Doctrine to generate migration file for us. 2822 | 2823 | If needed, we can add additional `UPDATE` or/and `INSERT` statements for data migration corresponding 2824 | to our new database structure. 2825 | 2826 | For new projects, we do not put `CREATE DATABASE` into migration scripts, but all tables should be created and 2827 | then altered by Doctrine migrations. 2828 | 2829 | ### ID column strategy 2830 | 2831 | We use `IDENTITY` strategy for ID generation. This makes different structure in PostgreSQL - it works in same way 2832 | as in MySQL. If we use `AUTO`, sequence is used and ID is set as soon as we persist the entity. 2833 | As this behavior is not reproducible in MySQL database, we use `IDENTITY` to be compatible with more databases. 2834 | This also affects functional tests as sqlite does not have sequences, too. 2835 | 2836 | Example configuration: 2837 | 2838 | ```xml 2839 | 2840 | 2841 | 2842 | ``` 2843 | 2844 | ### Variable types for query parameters 2845 | 2846 | Always make sure that parameters are of correct type when building query. 2847 | 2848 | This is especially important when we pass an integer and column type is `VARCHAR`. 2849 | In this case database tries to cast column value of every row into an integer and compare to passed value - no indexes 2850 | get used in such cases. For example: 2851 | 2852 | ```sql 2853 | SELECT * FROM ext_log_entries e0_ 2854 | WHERE e0_.object_id = 21590 2855 | AND e0_.object_class = 'Acme\\UserBundle\\Entity\\User' 2856 | ORDER BY e0_.version DESC; 2857 | ``` 2858 | 2859 | Correct usage: 2860 | 2861 | ```sql 2862 | SELECT * FROM ext_log_entries e0_ 2863 | WHERE e0_.object_id = '21590' 2864 | AND e0_.object_class = 'Acme\\UserBundle\\Entity\\User' 2865 | ORDER BY e0_.version DESC; 2866 | ``` 2867 | 2868 | ## Controllers 2869 | 2870 | ### As services 2871 | 2872 | We use controllers as services. That is, every controller must be defined as a service. 2873 | 2874 | If controller methods return Response objects (not for REST controllers), controller can extend base 2875 | Symfony Controller. In this case we add a call to setContainer in controller definition (in `services/controllers.xml`). 2876 | We do not use container in the controller’s code - we inject needed services via constructor. 2877 | 2878 | ### Static templates 2879 | 2880 | If controller has no logic, we use `FrameworkBundle:Template:template` controller in routing and set `template` 2881 | argument to twig template name to render. 2882 | 2883 | ### Parameters passed to view 2884 | 2885 | We pass original representation of variables to view, if view can itself transform the variables. 2886 | 2887 | ```php 2888 | render($template, array( 2898 | 'options' => array('a' => $a, 'b' => $b), // we do not use json_encode() here 2899 | )); 2900 | } 2901 | } 2902 | ``` 2903 | 2904 | ```twig 2905 |
2906 | ``` 2907 | 2908 | > **Why?** Controller just provides variables, it does not know (or shouldn’t know) how they will be used in template. 2909 | > Furthermore, template sometimes needs both encoded and original versions, so controller would duplicate some 2910 | > code in such case. 2911 | 2912 | 2913 | ### Small controllers 2914 | 2915 | We group actions into small controllers - we do not put more than 5-8 actions into one controller. 2916 | 2917 | We try to make controller for each of resources, not just one for whole bundle. 2918 | 2919 | It’s perfectly ok to have controllers containing only one action. 2920 | 2921 | > **Why?** As we use constructor injection, big controllers have many dependencies, which are rarely used in such cases. 2922 | > This is bad for performance. Furthermore, more methods make code more difficult and dependencies not that clear. 2923 | 2924 | ### Class naming 2925 | 2926 | Controller class name must always end with `Controller`. 2927 | 2928 | ## Events 2929 | 2930 | ### Available events 2931 | 2932 | We put all available events in `final` class, containing only constants with available events. 2933 | 2934 | ### Event naming 2935 | 2936 | We name events in past-tense verbs, prefixed by resource for which some action happened. 2937 | 2938 | > **Why?** Events should be used only after something happened (exception: before something happening). 2939 | > Present tense verb would indicate that event should make something, which would be a misuse of event system. 2940 | 2941 | We do not start constants with `ON_`. 2942 | 2943 | > **Why?** `on*` indicates action performed when event is dispatched, not the event itself. 2944 | 2945 | We can start constants with `BEFORE_` or `AFTER_` if it indicated event that is dispatched before or after some action. 2946 | 2947 | We give constant the same value as the constant name, prefixed with bundle name. 2948 | 2949 | > **Why?** For find-replace to find both constant and it’s value usage (in tags). 2950 | > Bundle prefix is needed to avoid collisions. 2951 | 2952 | 2953 | ```php 2954 | **Why not using 3 different major versions?** This would make quite hard to maintain several Symfony versions in 3135 | > our libraries and bundles. 3136 | 3137 | ### Configuration 3138 | 3139 | We always use skeleton to bootstrap new projects as this allows to easily use any (configuration) fixes or improvements 3140 | that were made to each and every project during the years. It also includes all needed basic features that could 3141 | be depended on by our libraries or bundles which could be installed afterwards. 3142 | 3143 | Keep in mind that there are different skeletons for WEB, REST API and processing nodes. 3144 | 3145 | ## REST in PHP 3146 | 3147 | ### REST controllers 3148 | 3149 | For REST controllers, we use [`PayseraApiBundle`](https://github.com/paysera/lib-api-bundle) and normalizers. 3150 | 3151 | Controller methods return entities or scalar variables. 3152 | 3153 | Each method is configured with normalizer and denormalizer if needed. 3154 | 3155 | We do not use request object from the controller. 3156 | We define normalizers to give only the needed information to the controller. 3157 | We can use plain normalizer or plain item normalizer if needed. 3158 | 3159 | ### Result providers 3160 | 3161 | We use result provider to give `Result` entities from REST controller. 3162 | 3163 | Response is automatically normalized using normalizer related to the classname of the returned object. 3164 | We could override it with additional annotation, if needed. 3165 | 3166 | ### Extending model 3167 | 3168 | In server side we do not use subclasses and class-maps - we use services that give or process needed information. 3169 | 3170 | On the client side, on the other hand, we have full model (via subclasses or just put into one class). 3171 | 3172 | On the server side: 3173 | 3174 | ```php 3175 | query('SELECT * FROM users WHERE id = ?', $user_id); 3305 | } 3306 | ``` 3307 | 3308 | We write some functionality that needs to get user's email and name. We could use legacy code directly like in the 3309 | following example, but we shouldn't: 3310 | 3311 | ```php 3312 | getUserId(); 3323 | $userData = get_user_data($userId); // don't do this! ;) 3324 | $userEmail = $userData['email']; 3325 | $userName = $userData['name']; 3326 | // ... 3327 | } 3328 | } 3329 | ``` 3330 | 3331 | Instead, we create an interface that provides our needed functionality: 3332 | 3333 | ```php 3334 | email = $email; 3363 | $this->name = $name; 3364 | } 3365 | 3366 | public function getEmail(): string 3367 | { 3368 | return $this->email; 3369 | } 3370 | 3371 | public function setEmail(string $email): self 3372 | { 3373 | $this->email = $email; 3374 | 3375 | return $this; 3376 | } 3377 | 3378 | public function getName(): string 3379 | { 3380 | return $this->name; 3381 | } 3382 | 3383 | public function setName(string $name): self 3384 | { 3385 | $this->name = $name; 3386 | return $this; 3387 | } 3388 | } 3389 | ``` 3390 | 3391 | And we refactor our service: 3392 | 3393 | ```php 3394 | userDataProvider = $userDataProvider; 3407 | } 3408 | 3409 | public function sendNewsletter(Newsletter $newsletter): void 3410 | { 3411 | $userData = $this->userDataProvider->getUserData($newsletter->getUserId()); 3412 | $userEmail = $userData->getEmail(); 3413 | $userName = $userData->getName(); 3414 | // ... 3415 | } 3416 | } 3417 | ``` 3418 | 3419 | This allows to have new code that has no dependencies on the legacy one. 3420 | 3421 | What we need to do next is implement the interface. To still keep *legacy code* and *current code* separate, 3422 | we do that in another bundle. 3423 | 3424 | 3425 | ```php 3426 | setEmail($userData['email']) 3445 | ->setName($userData['name']) 3446 | ; 3447 | } 3448 | } 3449 | ``` 3450 | 3451 | We configure the service as usual (inside `IntegrationBundle` in this case): 3452 | ```xml 3453 | 3455 | ``` 3456 | 3457 | For our bundle to get the service from outside, we modify `Configuration` and `Extension` classes and configure 3458 | the service in `config.yml`: 3459 | 3460 | ```php 3461 | root('acme_newsletter'); 3475 | $children = $rootNode->children(); 3476 | $children->scalarNode('user_data_provider')->isRequired(); 3477 | return $treeBuilder; 3478 | } 3479 | } 3480 | ``` 3481 | 3482 | ```php 3483 | processConfiguration($configuration, $configs); 3497 | 3498 | $loader = new XmlFileLoader($container, new FileLocator(__DIR__ . '/../Resources/config')); 3499 | $loader->load('services.xml'); 3500 | 3501 | $container->setAlias('acme_newsletter.user_data_provider', $config['user_data_provider']); 3502 | } 3503 | } 3504 | ``` 3505 | 3506 | ```yaml 3507 | acme_newsletter: 3508 | user_data_provider: integration.user_data_provider 3509 | ``` 3510 | 3511 | Now we can use our service inside our bundle: 3512 | 3513 | ```xml 3514 | 3516 | 3517 | 3518 | ``` 3519 | 3520 | ## Database migrations 3521 | 3522 | Even for database tables that are not managed by Doctrine, we use Doctrine migrations for migrating their structure. 3523 | This enables clear process for upgrading the database structure and makes it reproducible in all environments. 3524 | 3525 | 3526 | 3527 | 3528 | # Composer Conventions 3529 | 3530 | ## Semantic versioning 3531 | 3532 | We always use semantic versioning on library repositories. 3533 | 3534 | > **Why?** When backward incompatible change is made in library, we have two options: 3535 | > 1) update all client side projects with new library version; 3536 | > 2) every time when we update library check for previous backward incompatible changes, 3537 | > not related to currently added feature. 3538 | > 3539 | > As it happens, sometimes none of these 2 takes place. 3540 | 3541 | ## Changelog 3542 | 3543 | For libraries where semantic vensioning is used, we maintain the [`CHANGELOG.md` file](https://keepachangelog.com/en/1.0.0/). 3544 | 3545 | If the file is missing, we create it and port changes from other sources, like `UPGRADE.md` (deleting that file afterwards). 3546 | 3547 | Any commit must also have a change in `CHANGELOG.md` file with described changes. 3548 | 3549 | ## Releases 3550 | 3551 | We tag each and every commit (except instant bug-fixes or commits before separate merge commits). 3552 | 3553 | 1. Right before tagging, we make sure we're on master branch and always run `git tag --sort=v:refname` to see current 3554 | tags. 3555 | 2. Before tagging, we see available branches on repository as version can be created by branch alias, not only by tags. 3556 | 3. We bump MAJOR, MINOR or PATCH version by one from latest version available 3557 | (or parent commit if there are few active branches). 3558 | 3559 | 3.1. If we make backward incompatible change, we bump the MAJOR version. 3560 | 3561 | 3.2. If we add new feature (any new arguments, method calls, classes etc.), we bump MINOR version. 3562 | 3563 | 3.3. If we do not change API of library in any way, just change the internals (usually bug-fixes), 3564 | we bump PATCH version. 3565 | 3566 | We use annotated tags so that time and author would be present. Example command, `1.2.3` taken as example tag: 3567 | 3568 | ``` 3569 | git tag -a -m "" 1.2.3 3570 | ``` 3571 | 3572 | `-m ""` sets message to the tag, but as all commits are already with messages, we can leave it empty. 3573 | 3574 | After tagging we need to call `git push --tags` - this pushes our tags to origin. We only do this after we've 3575 | landed our changes. 3576 | 3577 | ## Reviewing tagging policy 3578 | 3579 | The tagging policy is always visible in `CHANGELOG.md` file - we don't use `Unreleased` block for internal libraries 3580 | that are updated for some concrete purpose (to be updated right afterwards in another project). 3581 | 3582 | It's important to keep it updated and in-sync if any other changes are made in master after our diff - one should 3583 | be careful when rebasing this file. 3584 | 3585 | ## Initial library development 3586 | 3587 | Until library is stable, versions can change quite often. In this case we still use tags and semantic versioning, 3588 | but use `0` as MAJOR version, which allows any backward incompatible change be made in any MINOR version bump. 3589 | 3590 | If API of library is already quite stable, we use `1` as a MAJOR version initially. 3591 | 3592 | ## Constraints on library versions 3593 | 3594 | We use as generic constraints as possible for required libraries. This avoids updating some library graph just because 3595 | of some conservative constraint, and not because it is really needed. 3596 | 3597 | For example, we have `lib-a@1.0.0` and `lib-b@1.0.0`, which depends on `lib-a: ^1.0`. 3598 | If we roll out MAJOR release for `lib-a` (aka `lib-a@2.0.0`), at some time we need to modify constraint on `lib-b`: 3599 | 3600 | 1. If `lib-b` works with both `lib-a@1.0.0` and `lib-a@2.0.0` (change did not affect this library), 3601 | we use `lib-a: >=1.0.0,<3.0`. This allows to use both `1.0` and `2.0` with our new version of `lib-b`. 3602 | 2. If `lib-b` works only with `lib-a@2.0.0` and not with `lib-a@1.0.0`, 3603 | we modify constraint as usual to `lib-a: ^2.0.0`. 3604 | 3. If `lib-b` does not work with `lib-a@2.0.0`, we can either leave constraint to `^1.0` 3605 | or update code and use (1) or (2) depending on compatibility with `1.0` version. 3606 | Latter is recommended, as this allows to use newest possible versions (sooner or later we will need to update them). 3607 | 3608 | Version in `lib-b` in any of these cases is not required to make a MAJOR bump - if API of `lib-b` is left the same, 3609 | we can make PATCH update. We can even entirely drop out `lib-a` and use some alternative library, as long as API 3610 | leaves the same. 3611 | 3612 | This means that if `app-x` (or any other library) uses functions or classes from `lib-a`, 3613 | it must require it in `composer.json`. 3614 | If it just depends on `lib-b`, it does not care about `lib-a` versions or even if it is installed at all. 3615 | 3616 | ## Constraints on vendors 3617 | 3618 | If we require vendor library, we use constraint depending on versioning strategy of that library. 3619 | If it states, that it follows semantic versioning (and we believe it does), 3620 | we use something like `^1.2.3` - this is added by default if we run `composer require vendor/library`. 3621 | 3622 | If it has some other strategy, we must correct the constraint so that we would not get unexpected backward incompatible 3623 | changes. For example, Symfony components did break compatibility on minor releases (up to 2.3 version). 3624 | In that case, we should have not used `^` in constraint. 3625 | 3626 | Basically, we must always assume that `composer update` can be run at any time and everything should still 3627 | work as expected. 3628 | 3629 | Due to the same reason, we never require `dev-master`. 3630 | If vendor library has no versions defined, we require specific commit: 3631 | 3632 | ``` 3633 | "vendor/package": "dev-master#8e45af93e4dc39c22effdc4cd6e5529e25c31735" 3634 | ``` 3635 | ## Library version incrementing 3636 | 3637 | If the public API does not change, but dependencies do, even if they are breaking backwards compatibility, it’s a minor version bump. 3638 | 3639 | ### Why? 3640 | As per https://semver.org/, version incrementing concerns itself only with the public API: 3641 | - Major version X (X.y.z | X > 0) MUST be incremented if any backwards incompatible changes are introduced to the public API. 3642 | - Minor version Y (x.Y.z | x > 0) MUST be incremented if new, backwards compatible functionality is introduced to the public API. 3643 | --------------------------------------------------------------------------------