├── CHANGELOG.md ├── LICENSE.txt ├── README.md ├── composer.json ├── config └── services.php ├── phpstan-extension.neon ├── src ├── Bundle │ └── DependencyInjection │ │ └── DoctrineBehaviorsExtension.php ├── Contract │ ├── Entity │ │ ├── BlameableInterface.php │ │ ├── LoggableInterface.php │ │ ├── SluggableInterface.php │ │ ├── SoftDeletableInterface.php │ │ ├── TimestampableInterface.php │ │ ├── TranslatableInterface.php │ │ ├── TranslationInterface.php │ │ ├── TreeNodeInterface.php │ │ └── UuidableInterface.php │ └── Provider │ │ ├── LocaleProviderInterface.php │ │ └── UserProviderInterface.php ├── DoctrineBehaviorsBundle.php ├── EventSubscriber │ ├── BlameableEventSubscriber.php │ ├── LoggableEventSubscriber.php │ ├── SluggableEventSubscriber.php │ ├── SoftDeletableEventSubscriber.php │ ├── TimestampableEventSubscriber.php │ ├── TranslatableEventSubscriber.php │ ├── TreeEventSubscriber.php │ └── UuidableEventSubscriber.php ├── Exception │ ├── ShouldNotHappenException.php │ ├── SluggableException.php │ ├── TranslatableException.php │ └── TreeException.php ├── Model │ ├── Blameable │ │ ├── BlameableMethodsTrait.php │ │ ├── BlameablePropertiesTrait.php │ │ └── BlameableTrait.php │ ├── Loggable │ │ └── LoggableTrait.php │ ├── Sluggable │ │ ├── SluggableMethodsTrait.php │ │ ├── SluggablePropertiesTrait.php │ │ └── SluggableTrait.php │ ├── SoftDeletable │ │ ├── SoftDeletableMethodsTrait.php │ │ ├── SoftDeletablePropertiesTrait.php │ │ └── SoftDeletableTrait.php │ ├── Timestampable │ │ ├── TimestampableMethodsTrait.php │ │ ├── TimestampablePropertiesTrait.php │ │ └── TimestampableTrait.php │ ├── Translatable │ │ ├── TranslatableMethodsTrait.php │ │ ├── TranslatablePropertiesTrait.php │ │ ├── TranslatableTrait.php │ │ ├── TranslationMethodsTrait.php │ │ ├── TranslationPropertiesTrait.php │ │ └── TranslationTrait.php │ ├── Tree │ │ ├── TreeNodeMethodsTrait.php │ │ ├── TreeNodePropertiesTrait.php │ │ └── TreeNodeTrait.php │ └── Uuidable │ │ ├── UuidableMethodsTrait.php │ │ ├── UuidablePropertiesTrait.php │ │ └── UuidableTrait.php ├── ORM │ └── Tree │ │ └── TreeTrait.php ├── Provider │ ├── LocaleProvider.php │ └── UserProvider.php └── Repository │ └── DefaultSluggableRepository.php └── utils └── phpstan-behaviors └── src ├── Exception └── PHPStanTypeException.php └── Type ├── StaticTranslationTypeHelper.php ├── TranslatableGetTranslationsDynamicMethodReturnTypeExtension.php ├── TranslatableTranslateDynamicMethodReturnTypeExtension.php └── TranslationGetTranslatableDynamicMethodReturnTypeExtension.php /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) 6 | and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). 7 | 8 | PRs and issues are linked, so you can find more about it. Thanks to [ChangelogLinker](https://github.com/Symplify/ChangelogLinker). 9 | 10 | 11 | 12 | ## [v2.0.1] - 2020-01-08 13 | 14 | ### Changed 15 | 16 | - [#493] Moved unitOfWork access to methods instead of constructor, Thanks to [@toooni] 17 | - [#484] Update trait names in docs, Thanks to [@ckrack] 18 | 19 | ### Fixed 20 | 21 | - [#490] fix boolean method name prefix 22 | - [#487] Fix slug uniqueness, Thanks to [@hermann8u] 23 | 24 | ## [v2.0.0] - 2020-01-02 25 | 26 | ### Changed 27 | 28 | - [#479] make lazy loading configurable 29 | 30 | ### Removed 31 | 32 | - [#480] [Translatable] Remove mapId() magic 33 | 34 | ## [v2.0.0-beta1] - 2020-01-01 35 | 36 | ### Added 37 | 38 | - [#470] Add `CHANGELOG.md` 39 | 40 | ### Changed 41 | 42 | - [#475] [Tree] Include name "tree" in naming 43 | - [#478] Improve docs, handle timestampable field type for the user 44 | - [#474] split NodeTrait to NodeMethodsTrait and NodePropertiesTrait 45 | 46 | ### Fixed 47 | 48 | - [#477] Fix slug uniqueness check function, Thanks to [@StanislavUngr] 49 | - [#472] Fix slug generation if the getRegenerateSlugOnUpdate method return false, Thanks to [@hermann8u] 50 | 51 | ### Removed 52 | 53 | - [#473] [Sortable] Drop, it never worked 54 | 55 | ## [v2.0.0-alpha4] - 2019-12-24 56 | 57 | ### Added 58 | 59 | - [#469] [Rector] Add Upgrade set for id property on translations 60 | 61 | ## [v2.0.0-alpha3] - 2019-12-19 62 | 63 | ### Changed 64 | 65 | - [#468] Use symfony/strings instead of transliterator 66 | 67 | ### Removed 68 | 69 | - [#467] [Geocodable] Drop for very wide interface and limited usage 70 | - [#464] Remove scheduleExtraUpdate calls 71 | 72 | ## [v2.0.0-alpha2] - 2019-12-18 73 | 74 | ### Added 75 | 76 | - [#448] [Translation] add abstract class support 77 | - [#460] add setTranslations() 78 | - [#452] Add missing dependency symplify/package-builder, Thanks to [@webda2l] 79 | - [#447] add default location provider 80 | 81 | ### Changed 82 | 83 | - [#458] Use PHP 7.4 instead of a snapshot on Travis, Thanks to [@andreybolonin] 84 | - [#459] composer: use the symfony/security for symfony 4.4 85 | - [#461] Various updates 86 | - [#449] make slug unique optionally [closes [#236]] 87 | - [#453] use entity list instead of explicit 88 | - [#450] [Uuidable] init 89 | - [#451] [tests] move entities and repositories to own namespaces 90 | 91 | ### Removed 92 | 93 | - [#463] drop filterable, way to opinionated and limited, use custom implementation 94 | - [#457] remove repository traits, use custom methods in own repository instead 95 | 96 | ## [v2.0.0-alpha1] - 2019-12-12 97 | 98 | ### Added 99 | 100 | - [#435] [CI] Add + Apply Coding Standards: PSR12, PHP 7.0, PHP 7.1 101 | - [#445] [CI] Add Rector 102 | - [#436] Add static code analysis and PSR-4 for tests 103 | - [#443] Add code of conduct 104 | - [#425] Explicitly add maintainers in the README, Thanks to [@alexpozzi] 105 | 106 | ### Changed 107 | 108 | - [#423] Do not specify version constraint - let Composer do this, Thanks to [@bocharsky-bw] 109 | - [#433] Travis: bump to min PHP 7.2, test stable doctrine/orm 110 | - [#389] Shrink locale columns to 5 chars, Thanks to [@NiR-] 111 | - [#442] Refactoring tests to dependency injection container based + use interfaces over traits for detection 112 | - [#390] Document master and v1 branches, Thanks to [@NiR-] 113 | - [#438] [cs] apply common set - unite MIT license to single file 114 | - [#444] [cs] use trait suffix for traits to prevent opening 115 | - [#439] [cs] apply symplify set 116 | - [#441] [tests] strict types for subscribers, various PR cherry-pick 117 | 118 | ### Fixed 119 | 120 | - [#440] [Translatable] Fix property access on twig 121 | - [#411] Fix config deprecation, Thanks to [@martinprihoda] 122 | 123 | ## [1.6.0] - 2018-11-13 124 | 125 | ### Added 126 | 127 | - [#358] [Tree] Add possibility to pass extra parameters in getTree, Thanks to [@Einenlum] 128 | 129 | ### Changed 130 | 131 | - [#382] [Translatable] Do not persist new translations if empty, Thanks to [@giuliapellegrini] 132 | - [#392] Only set locales on entities managed by knp translations, Thanks to [@jordisala1991] 133 | 134 | ## [1.5.0] - 2017-09-27 135 | 136 | ### Added 137 | 138 | - [#361] Add nullable setchildof, Thanks to [@Einenlum] 139 | 140 | ### Changed 141 | 142 | - [#363] Looking for maintainers, Thanks to [@Einenlum] 143 | 144 | ### Fixed 145 | 146 | - [#365] Fix drop php < 7, Thanks to [@Einenlum] 147 | 148 | ## [1.4.1] - 2017-09-19 149 | 150 | ### Changed 151 | 152 | - [#338] Update branch alias in composer.json, Thanks to [@nykopol] 153 | - [#326] Use svg image for Travis badge and show status of master branch, Thanks to [@bocharsky-bw] 154 | - [#328] Run PHPUnit in normal mode instead of --testdox, Thanks to [@bocharsky-bw] 155 | 156 | ### Fixed 157 | 158 | - [#304] Minor fix: Tweak docblocks, Thanks to [@bocharsky-bw] 159 | - [#256] fix typo, Thanks to [@shieldo] 160 | - [#332] Fix: isEmpty() return true if it's empty, Thanks to [@corentinheadoo] 161 | - [#360] Fix doctrine dependency and drop PHP < 7, Thanks to [@Einenlum] 162 | - [#350] Fix disabling softdeletable, Thanks to [@ossinkine] 163 | - [#353] Markdown syntax fix, Thanks to [@Nyholm] 164 | 165 | ## [1.4.0] - 2016-09-30 166 | 167 | - [#317] Fix interaction between translations & joined inheritance, Thanks to [@lemoinem] 168 | - [#316] Fixes for Symfony 3.1, Thanks to [@tarlepp] 169 | 170 | ## Previous Versions 171 | 172 | ### Added 173 | 174 | - [#98] [Geocodable] Add a function to compute distances in meters., Thanks to [@kimlai] 175 | - [#262] Timestampable - add db field type parameter, Thanks to [@lopsided] 176 | - [#1] Added filterable repository behavior, Thanks to [@l3l0] 177 | - [#253] Add documentation to override the default naming strategy for translatable, Thanks to [@ksom] 178 | - [#76] Add missing setSlug method, Thanks to [@EmmanuelVella] 179 | - [#10] Added sluggable trait, Thanks to [@Lusitanian] 180 | - [#20] Add a post delete feature to the SoftDeletable trait, Thanks to [@PedroTroller] 181 | - [#25] Add a method to test the removal of the object in the future, Thanks to [@PedroTroller] 182 | - [#27] Add preRemove hook to Blamable trait and listener, Thanks to [@PedroTroller] 183 | - [#57] Add a creation message, Thanks to [@PedroTroller] 184 | - [#62] add recursive trait parameter to orm services, Thanks to [@docteurklein] 185 | - [#112] Adds a parameter for the fetch method used by doctrine for the translations, Thanks to [@bobvandevijver] 186 | - [#220] Add documentation about restore() method in Softdeleteable, Thanks to [@akovalyov] 187 | - [#138] add several missing subscribers, Thanks to [@greg0ire] 188 | - [#148] travis - PHP 5.6 added, linter added 189 | - [#160] Return $this where it was not added., Thanks to [@kuczek] 190 | - [#179] Improved Entity Managers configs + added doc for testing from local env, Thanks to [@hanovruslan] 191 | - [#189] added customizable tree identifier, Thanks to [@digitalkaoz] 192 | - [#191] Add Callable function to override default language, Thanks to [@jerome-fix] 193 | - [#206] travis: PHP 7.0 nightly added 194 | - [#243] Add missing annotations, Thanks to [@bocharsky-bw] 195 | - [#192] added missing function to interface, Thanks to [@digitalkaoz] 196 | - [#18] [softDeletable] added method to restore a deleted entity, Thanks to [@inoryy] 197 | 198 | ### Changed 199 | 200 | - [#85] [FEATURE] Refactor \Knp\DoctrineBehaviors\ORM\Tree\Tree::getRootNodes to support QueryBuilder customization, Thanks to [@MisatoTremor] 201 | - [#187] Call generateSlug from SluggableSubscriber, Thanks to [@EmmanuelVella] 202 | - [#174] Fix ability to choose the class translation name, Thanks to [@asprega] 203 | - [#31] README update, Thanks to [@eillarra] 204 | - [#217] Change allowed_falures to allow_failures in travis.yml config, Thanks to [@akovalyov] 205 | - [#216] Attemt to put vendor folder to cache to prevent composer failures, Thanks to [@akovalyov] 206 | - [#56] Error serializing the AbstractToken in Symfony2, Thanks to [@patxi1980] 207 | - [#199] run tests with the lowest possible versions, Thanks to [@greg0ire] 208 | - [#36] Update TimestampableListener.php, Thanks to [@trsteel88] 209 | - [#114] ClassAnalyzer::hasTrait returns false if $parentClass is NULL 210 | - [#115] option to prevent default translation search, Thanks to [@DerekRoth] 211 | - [#119] Update README.md, Thanks to [@Mondane] 212 | - [#185] Update README.md, Thanks to [@JoydS] 213 | - [#126] Tests should be green., Thanks to [@akovalyov] 214 | - [#176] Yaml-Lint for travis., Thanks to [@kuczek] 215 | - [#162] Slug generation from cyrillic strings, Thanks to [@MAXakaWIZARD] 216 | - [#167] Allow locale as an association entity, Thanks to [@burci] 217 | - [#225] Highlight PHP code syntax, Thanks to [@bocharsky-bw] 218 | - [#161] Try to use getters when getting non-existent field values for Sluggable, Thanks to [@MAXakaWIZARD] 219 | - [#43] Update child methods so they contain 'Node', Thanks to [@trsteel88] 220 | - [#131] Ease contributions, Thanks to [@greg0ire] 221 | - [#157] Translation fallback, Thanks to [@kuczek] 222 | - [#133] use PSR-4 autoloading, Thanks to [@greg0ire] 223 | - [#149] TranslatableSubscriber change undefined property $this->em to $em, Thanks to [@adrienrusso] 224 | - [#44] Semantic versioning, Thanks to [@jankramer] 225 | - [#134] rename listener to subscriber, Thanks to [@greg0ire] 226 | - [#52] Update the geocodable documentation, Thanks to [@josselinh] 227 | - [#144] Improve the translatable documentation, Thanks to [@roukmoute] 228 | - [#135] Replace most occurences of listener with subscribers, Thanks to [@greg0ire] 229 | - [#136] Use event system, Thanks to [@greg0ire] 230 | - [#221] Register it as a bundle, Thanks to [@akovalyov] 231 | - [#226] Use PropertyAccessor for get translations, Thanks to [@bocharsky-bw] 232 | - [#55] Update README.md, Thanks to [@jalopezcar] 233 | - [#228] Change mysite.com with example.com, Thanks to [@bocharsky-bw] 234 | - [#2] Use non-locale aware type modifier %F in sprintf(), Thanks to [@jsor] 235 | - [#3] require php >= 5.4.0 since you use traits :), Thanks to [@pminnieur] 236 | - [#78] Make timestampable and blameable setters fluent, Thanks to [@EmmanuelVella] 237 | - [#6] rename entity traits to Model namespace, Thanks to [@docteurklein] 238 | - [#7] Auto metadata, Thanks to [@docteurklein] 239 | - [#13] Modify SoftDeletable, Thanks to [@akia] 240 | - [#14] Update README.md, Thanks to [@michelsalib] 241 | - [#290] Test against three last doctrine common versions, Thanks to [@akovalyov] 242 | - [#288] TranslatableMethods: use late static bindings, Thanks to [@meyerbaptiste] 243 | - [#284] Microseconds, Thanks to [@boekkooi] 244 | - [#276] Update UserCallable.php, Thanks to [@adampiotrowski] 245 | - [#89] Parametrized translatable and translation Traits, Thanks to [@alch] 246 | - [#274] Translatable: enable cascade persist and merge on the owning side, Thanks to [@jonasgoderis] 247 | - [#102] Parametrized traits, Thanks to [@gaydarov] 248 | - [#231] use psr logger instead of symfony logger, Thanks to [@digitalkaoz] 249 | - [#232] Make Blameable respect the isRecursive setting, Thanks to [@jdachtera] 250 | - [#240] Set current and default locale on prePersist event, Thanks to [@MAXakaWIZARD] 251 | - [#111] Rename of parameter to comply with the rest of the parameters, Thanks to [@bobvandevijver] 252 | - [#250] Reorder list of behaviors with ASC order in docs, Thanks to [@bocharsky-bw] 253 | - [#252] composer: bump to PHPUnit ~4.8 254 | - [#141] test different versions of doctrine, Thanks to [@greg0ire] 255 | - [#266] Make Behaviors configurable., Thanks to [@NiR-] 256 | - [#254] Update tree documentation, Thanks to [@ksom] 257 | - [#93] issue Bug (typo+) in softDeletable doc [#90], Thanks to [@siciarek] 258 | - [#21] Refactoring/listener, Thanks to [@PedroTroller] 259 | - [#265] Language only fallback, Thanks to [@DerekRoth] 260 | 261 | ### Fixed 262 | 263 | - [#88] [RFC] fix for TranslatableListener, to ignore entities that have translations as properties, Thanks to [@theodorDiaconu] 264 | - [#132] fix path, Thanks to [@greg0ire] 265 | - [#83] Fixed error with deletedBy field update on entity persist, Thanks to [@dmishh] 266 | - [#146] Fixed support for PointType with Mysql., Thanks to [@kuczek] 267 | - [#77] Fix phpdoc type hint, Thanks to [@EmmanuelVella] 268 | - [#30] Fixed bug with isDeleted() return true, Thanks to [@dmishh] 269 | - [#4] Fixes generate:doctrine:entities errors, Thanks to [@fixe] 270 | - [#300] Fix beforeNormalization anonymous function, Thanks to [@NiR-] 271 | - [#298] Fix if no config specified all behaviors are enabled, Thanks to [@NiR-] 272 | - [#279] Fixes Travis checks, Thanks to [@tobias-93] 273 | - [#275] Fixes compatibility for Symfony 3.0, Thanks to [@tobias-93] 274 | - [#24] Fixed removal of translations, Thanks to [@jankramer] 275 | - [#26] Fix/travis, Thanks to [@PedroTroller] 276 | - [#32] Fix DI mistake, Thanks to [@NicolasBadey] 277 | - [#143] Fixed typo in variable $fetchMode name, Thanks to [@cblegare] 278 | - [#37] Fix typos, Thanks to [@trsteel88] 279 | - [#40] Fixed SoftDeletable behavior when using inheritance, Thanks to [@jankramer] 280 | - [#68] Debug instead of log, fix for DateTime, Thanks to [@kuczek] 281 | - [#41] Fix Geocodable listener isEntitySupported check, Thanks to [@EmmanuelVella] 282 | - [#155] This PR is fixing [#122] and [#150], Thanks to [@pmontoya] 283 | - [#152] Fix for array values in log, Thanks to [@kuczek] 284 | - [#147] Fix missing EntityManager in TranslatableSubscriber 285 | 286 | ### Removed 287 | 288 | - [#60] Remove [@constructor] annotations, Thanks to [@jankramer] 289 | - [#158] Removed Id from EnittyTranslation fixture., Thanks to [@kuczek] 290 | - [#142] remove unneeded dependency, Thanks to [@greg0ire] 291 | 292 | [#469]: https://github.com/KnpLabs/DoctrineBehaviors/pull/469 293 | [#468]: https://github.com/KnpLabs/DoctrineBehaviors/pull/468 294 | [#467]: https://github.com/KnpLabs/DoctrineBehaviors/pull/467 295 | [#464]: https://github.com/KnpLabs/DoctrineBehaviors/pull/464 296 | [#463]: https://github.com/KnpLabs/DoctrineBehaviors/pull/463 297 | [#461]: https://github.com/KnpLabs/DoctrineBehaviors/pull/461 298 | [#460]: https://github.com/KnpLabs/DoctrineBehaviors/pull/460 299 | [#459]: https://github.com/KnpLabs/DoctrineBehaviors/pull/459 300 | [#458]: https://github.com/KnpLabs/DoctrineBehaviors/pull/458 301 | [#457]: https://github.com/KnpLabs/DoctrineBehaviors/pull/457 302 | [#453]: https://github.com/KnpLabs/DoctrineBehaviors/pull/453 303 | [#452]: https://github.com/KnpLabs/DoctrineBehaviors/pull/452 304 | [#451]: https://github.com/KnpLabs/DoctrineBehaviors/pull/451 305 | [#450]: https://github.com/KnpLabs/DoctrineBehaviors/pull/450 306 | [#449]: https://github.com/KnpLabs/DoctrineBehaviors/pull/449 307 | [#448]: https://github.com/KnpLabs/DoctrineBehaviors/pull/448 308 | [#447]: https://github.com/KnpLabs/DoctrineBehaviors/pull/447 309 | [#445]: https://github.com/KnpLabs/DoctrineBehaviors/pull/445 310 | [#444]: https://github.com/KnpLabs/DoctrineBehaviors/pull/444 311 | [#443]: https://github.com/KnpLabs/DoctrineBehaviors/pull/443 312 | [#442]: https://github.com/KnpLabs/DoctrineBehaviors/pull/442 313 | [#441]: https://github.com/KnpLabs/DoctrineBehaviors/pull/441 314 | [#440]: https://github.com/KnpLabs/DoctrineBehaviors/pull/440 315 | [#439]: https://github.com/KnpLabs/DoctrineBehaviors/pull/439 316 | [#438]: https://github.com/KnpLabs/DoctrineBehaviors/pull/438 317 | [#436]: https://github.com/KnpLabs/DoctrineBehaviors/pull/436 318 | [#435]: https://github.com/KnpLabs/DoctrineBehaviors/pull/435 319 | [#433]: https://github.com/KnpLabs/DoctrineBehaviors/pull/433 320 | [#425]: https://github.com/KnpLabs/DoctrineBehaviors/pull/425 321 | [#423]: https://github.com/KnpLabs/DoctrineBehaviors/pull/423 322 | [#411]: https://github.com/KnpLabs/DoctrineBehaviors/pull/411 323 | [#392]: https://github.com/KnpLabs/DoctrineBehaviors/pull/392 324 | [#390]: https://github.com/KnpLabs/DoctrineBehaviors/pull/390 325 | [#389]: https://github.com/KnpLabs/DoctrineBehaviors/pull/389 326 | [#382]: https://github.com/KnpLabs/DoctrineBehaviors/pull/382 327 | [#365]: https://github.com/KnpLabs/DoctrineBehaviors/pull/365 328 | [#363]: https://github.com/KnpLabs/DoctrineBehaviors/pull/363 329 | [#361]: https://github.com/KnpLabs/DoctrineBehaviors/pull/361 330 | [#360]: https://github.com/KnpLabs/DoctrineBehaviors/pull/360 331 | [#358]: https://github.com/KnpLabs/DoctrineBehaviors/pull/358 332 | [#353]: https://github.com/KnpLabs/DoctrineBehaviors/pull/353 333 | [#350]: https://github.com/KnpLabs/DoctrineBehaviors/pull/350 334 | [#338]: https://github.com/KnpLabs/DoctrineBehaviors/pull/338 335 | [#332]: https://github.com/KnpLabs/DoctrineBehaviors/pull/332 336 | [#328]: https://github.com/KnpLabs/DoctrineBehaviors/pull/328 337 | [#326]: https://github.com/KnpLabs/DoctrineBehaviors/pull/326 338 | [#317]: https://github.com/KnpLabs/DoctrineBehaviors/pull/317 339 | [#316]: https://github.com/KnpLabs/DoctrineBehaviors/pull/316 340 | [#304]: https://github.com/KnpLabs/DoctrineBehaviors/pull/304 341 | [#300]: https://github.com/KnpLabs/DoctrineBehaviors/pull/300 342 | [#298]: https://github.com/KnpLabs/DoctrineBehaviors/pull/298 343 | [#290]: https://github.com/KnpLabs/DoctrineBehaviors/pull/290 344 | [#288]: https://github.com/KnpLabs/DoctrineBehaviors/pull/288 345 | [#284]: https://github.com/KnpLabs/DoctrineBehaviors/pull/284 346 | [#279]: https://github.com/KnpLabs/DoctrineBehaviors/pull/279 347 | [#276]: https://github.com/KnpLabs/DoctrineBehaviors/pull/276 348 | [#275]: https://github.com/KnpLabs/DoctrineBehaviors/pull/275 349 | [#274]: https://github.com/KnpLabs/DoctrineBehaviors/pull/274 350 | [#266]: https://github.com/KnpLabs/DoctrineBehaviors/pull/266 351 | [#265]: https://github.com/KnpLabs/DoctrineBehaviors/pull/265 352 | [#262]: https://github.com/KnpLabs/DoctrineBehaviors/pull/262 353 | [#256]: https://github.com/KnpLabs/DoctrineBehaviors/pull/256 354 | [#254]: https://github.com/KnpLabs/DoctrineBehaviors/pull/254 355 | [#253]: https://github.com/KnpLabs/DoctrineBehaviors/pull/253 356 | [#252]: https://github.com/KnpLabs/DoctrineBehaviors/pull/252 357 | [#250]: https://github.com/KnpLabs/DoctrineBehaviors/pull/250 358 | [#243]: https://github.com/KnpLabs/DoctrineBehaviors/pull/243 359 | [#240]: https://github.com/KnpLabs/DoctrineBehaviors/pull/240 360 | [#236]: https://github.com/KnpLabs/DoctrineBehaviors/pull/236 361 | [#232]: https://github.com/KnpLabs/DoctrineBehaviors/pull/232 362 | [#231]: https://github.com/KnpLabs/DoctrineBehaviors/pull/231 363 | [#228]: https://github.com/KnpLabs/DoctrineBehaviors/pull/228 364 | [#226]: https://github.com/KnpLabs/DoctrineBehaviors/pull/226 365 | [#225]: https://github.com/KnpLabs/DoctrineBehaviors/pull/225 366 | [#221]: https://github.com/KnpLabs/DoctrineBehaviors/pull/221 367 | [#220]: https://github.com/KnpLabs/DoctrineBehaviors/pull/220 368 | [#217]: https://github.com/KnpLabs/DoctrineBehaviors/pull/217 369 | [#216]: https://github.com/KnpLabs/DoctrineBehaviors/pull/216 370 | [#206]: https://github.com/KnpLabs/DoctrineBehaviors/pull/206 371 | [#199]: https://github.com/KnpLabs/DoctrineBehaviors/pull/199 372 | [#192]: https://github.com/KnpLabs/DoctrineBehaviors/pull/192 373 | [#191]: https://github.com/KnpLabs/DoctrineBehaviors/pull/191 374 | [#189]: https://github.com/KnpLabs/DoctrineBehaviors/pull/189 375 | [#187]: https://github.com/KnpLabs/DoctrineBehaviors/pull/187 376 | [#185]: https://github.com/KnpLabs/DoctrineBehaviors/pull/185 377 | [#179]: https://github.com/KnpLabs/DoctrineBehaviors/pull/179 378 | [#176]: https://github.com/KnpLabs/DoctrineBehaviors/pull/176 379 | [#174]: https://github.com/KnpLabs/DoctrineBehaviors/pull/174 380 | [#167]: https://github.com/KnpLabs/DoctrineBehaviors/pull/167 381 | [#162]: https://github.com/KnpLabs/DoctrineBehaviors/pull/162 382 | [#161]: https://github.com/KnpLabs/DoctrineBehaviors/pull/161 383 | [#160]: https://github.com/KnpLabs/DoctrineBehaviors/pull/160 384 | [#158]: https://github.com/KnpLabs/DoctrineBehaviors/pull/158 385 | [#157]: https://github.com/KnpLabs/DoctrineBehaviors/pull/157 386 | [#155]: https://github.com/KnpLabs/DoctrineBehaviors/pull/155 387 | [#152]: https://github.com/KnpLabs/DoctrineBehaviors/pull/152 388 | [#150]: https://github.com/KnpLabs/DoctrineBehaviors/pull/150 389 | [#149]: https://github.com/KnpLabs/DoctrineBehaviors/pull/149 390 | [#148]: https://github.com/KnpLabs/DoctrineBehaviors/pull/148 391 | [#147]: https://github.com/KnpLabs/DoctrineBehaviors/pull/147 392 | [#146]: https://github.com/KnpLabs/DoctrineBehaviors/pull/146 393 | [#144]: https://github.com/KnpLabs/DoctrineBehaviors/pull/144 394 | [#143]: https://github.com/KnpLabs/DoctrineBehaviors/pull/143 395 | [#142]: https://github.com/KnpLabs/DoctrineBehaviors/pull/142 396 | [#141]: https://github.com/KnpLabs/DoctrineBehaviors/pull/141 397 | [#138]: https://github.com/KnpLabs/DoctrineBehaviors/pull/138 398 | [#136]: https://github.com/KnpLabs/DoctrineBehaviors/pull/136 399 | [#135]: https://github.com/KnpLabs/DoctrineBehaviors/pull/135 400 | [#134]: https://github.com/KnpLabs/DoctrineBehaviors/pull/134 401 | [#133]: https://github.com/KnpLabs/DoctrineBehaviors/pull/133 402 | [#132]: https://github.com/KnpLabs/DoctrineBehaviors/pull/132 403 | [#131]: https://github.com/KnpLabs/DoctrineBehaviors/pull/131 404 | [#126]: https://github.com/KnpLabs/DoctrineBehaviors/pull/126 405 | [#122]: https://github.com/KnpLabs/DoctrineBehaviors/pull/122 406 | [#119]: https://github.com/KnpLabs/DoctrineBehaviors/pull/119 407 | [#115]: https://github.com/KnpLabs/DoctrineBehaviors/pull/115 408 | [#114]: https://github.com/KnpLabs/DoctrineBehaviors/pull/114 409 | [#112]: https://github.com/KnpLabs/DoctrineBehaviors/pull/112 410 | [#111]: https://github.com/KnpLabs/DoctrineBehaviors/pull/111 411 | [#102]: https://github.com/KnpLabs/DoctrineBehaviors/pull/102 412 | [#98]: https://github.com/KnpLabs/DoctrineBehaviors/pull/98 413 | [#93]: https://github.com/KnpLabs/DoctrineBehaviors/pull/93 414 | [#90]: https://github.com/KnpLabs/DoctrineBehaviors/pull/90 415 | [#89]: https://github.com/KnpLabs/DoctrineBehaviors/pull/89 416 | [#88]: https://github.com/KnpLabs/DoctrineBehaviors/pull/88 417 | [#85]: https://github.com/KnpLabs/DoctrineBehaviors/pull/85 418 | [#83]: https://github.com/KnpLabs/DoctrineBehaviors/pull/83 419 | [#78]: https://github.com/KnpLabs/DoctrineBehaviors/pull/78 420 | [#77]: https://github.com/KnpLabs/DoctrineBehaviors/pull/77 421 | [#76]: https://github.com/KnpLabs/DoctrineBehaviors/pull/76 422 | [#68]: https://github.com/KnpLabs/DoctrineBehaviors/pull/68 423 | [#62]: https://github.com/KnpLabs/DoctrineBehaviors/pull/62 424 | [#60]: https://github.com/KnpLabs/DoctrineBehaviors/pull/60 425 | [#57]: https://github.com/KnpLabs/DoctrineBehaviors/pull/57 426 | [#56]: https://github.com/KnpLabs/DoctrineBehaviors/pull/56 427 | [#55]: https://github.com/KnpLabs/DoctrineBehaviors/pull/55 428 | [#52]: https://github.com/KnpLabs/DoctrineBehaviors/pull/52 429 | [#44]: https://github.com/KnpLabs/DoctrineBehaviors/pull/44 430 | [#43]: https://github.com/KnpLabs/DoctrineBehaviors/pull/43 431 | [#41]: https://github.com/KnpLabs/DoctrineBehaviors/pull/41 432 | [#40]: https://github.com/KnpLabs/DoctrineBehaviors/pull/40 433 | [#37]: https://github.com/KnpLabs/DoctrineBehaviors/pull/37 434 | [#36]: https://github.com/KnpLabs/DoctrineBehaviors/pull/36 435 | [#32]: https://github.com/KnpLabs/DoctrineBehaviors/pull/32 436 | [#31]: https://github.com/KnpLabs/DoctrineBehaviors/pull/31 437 | [#30]: https://github.com/KnpLabs/DoctrineBehaviors/pull/30 438 | [#27]: https://github.com/KnpLabs/DoctrineBehaviors/pull/27 439 | [#26]: https://github.com/KnpLabs/DoctrineBehaviors/pull/26 440 | [#25]: https://github.com/KnpLabs/DoctrineBehaviors/pull/25 441 | [#24]: https://github.com/KnpLabs/DoctrineBehaviors/pull/24 442 | [#21]: https://github.com/KnpLabs/DoctrineBehaviors/pull/21 443 | [#20]: https://github.com/KnpLabs/DoctrineBehaviors/pull/20 444 | [#18]: https://github.com/KnpLabs/DoctrineBehaviors/pull/18 445 | [#14]: https://github.com/KnpLabs/DoctrineBehaviors/pull/14 446 | [#13]: https://github.com/KnpLabs/DoctrineBehaviors/pull/13 447 | [#10]: https://github.com/KnpLabs/DoctrineBehaviors/pull/10 448 | [#7]: https://github.com/KnpLabs/DoctrineBehaviors/pull/7 449 | [#6]: https://github.com/KnpLabs/DoctrineBehaviors/pull/6 450 | [#4]: https://github.com/KnpLabs/DoctrineBehaviors/pull/4 451 | [#3]: https://github.com/KnpLabs/DoctrineBehaviors/pull/3 452 | [#2]: https://github.com/KnpLabs/DoctrineBehaviors/pull/2 453 | [#1]: https://github.com/KnpLabs/DoctrineBehaviors/pull/1 454 | [v2.0.0-alpha4]: https://github.com/KnpLabs/DoctrineBehaviors/compare/v2.0.0-alpha3...v2.0.0-alpha4 455 | [v2.0.0-alpha3]: https://github.com/KnpLabs/DoctrineBehaviors/compare/v2.0.0-alpha2...v2.0.0-alpha3 456 | [v2.0.0-alpha2]: https://github.com/KnpLabs/DoctrineBehaviors/compare/v2.0.0-alpha1...v2.0.0-alpha2 457 | [@webda2l]: https://github.com/webda2l 458 | [@trsteel88]: https://github.com/trsteel88 459 | [@tobias-93]: https://github.com/tobias-93 460 | [@theodorDiaconu]: https://github.com/theodorDiaconu 461 | [@tarlepp]: https://github.com/tarlepp 462 | [@siciarek]: https://github.com/siciarek 463 | [@shieldo]: https://github.com/shieldo 464 | [@roukmoute]: https://github.com/roukmoute 465 | [@pmontoya]: https://github.com/pmontoya 466 | [@pminnieur]: https://github.com/pminnieur 467 | [@patxi1980]: https://github.com/patxi1980 468 | [@ossinkine]: https://github.com/ossinkine 469 | [@nykopol]: https://github.com/nykopol 470 | [@michelsalib]: https://github.com/michelsalib 471 | [@meyerbaptiste]: https://github.com/meyerbaptiste 472 | [@martinprihoda]: https://github.com/martinprihoda 473 | [@lopsided]: https://github.com/lopsided 474 | [@lemoinem]: https://github.com/lemoinem 475 | [@l3l0]: https://github.com/l3l0 476 | [@kuczek]: https://github.com/kuczek 477 | [@ksom]: https://github.com/ksom 478 | [@kimlai]: https://github.com/kimlai 479 | [@jsor]: https://github.com/jsor 480 | [@josselinh]: https://github.com/josselinh 481 | [@jordisala1991]: https://github.com/jordisala1991 482 | [@jonasgoderis]: https://github.com/jonasgoderis 483 | [@jerome-fix]: https://github.com/jerome-fix 484 | [@jdachtera]: https://github.com/jdachtera 485 | [@jankramer]: https://github.com/jankramer 486 | [@jalopezcar]: https://github.com/jalopezcar 487 | [@inoryy]: https://github.com/inoryy 488 | [@hanovruslan]: https://github.com/hanovruslan 489 | [@greg0ire]: https://github.com/greg0ire 490 | [@giuliapellegrini]: https://github.com/giuliapellegrini 491 | [@gaydarov]: https://github.com/gaydarov 492 | [@fixe]: https://github.com/fixe 493 | [@eillarra]: https://github.com/eillarra 494 | [@docteurklein]: https://github.com/docteurklein 495 | [@dmishh]: https://github.com/dmishh 496 | [@digitalkaoz]: https://github.com/digitalkaoz 497 | [@corentinheadoo]: https://github.com/corentinheadoo 498 | [@constructor]: https://github.com/constructor 499 | [@cblegare]: https://github.com/cblegare 500 | [@burci]: https://github.com/burci 501 | [@boekkooi]: https://github.com/boekkooi 502 | [@bocharsky-bw]: https://github.com/bocharsky-bw 503 | [@bobvandevijver]: https://github.com/bobvandevijver 504 | [@asprega]: https://github.com/asprega 505 | [@andreybolonin]: https://github.com/andreybolonin 506 | [@alexpozzi]: https://github.com/alexpozzi 507 | [@alch]: https://github.com/alch 508 | [@akovalyov]: https://github.com/akovalyov 509 | [@akia]: https://github.com/akia 510 | [@adrienrusso]: https://github.com/adrienrusso 511 | [@adampiotrowski]: https://github.com/adampiotrowski 512 | [@PedroTroller]: https://github.com/PedroTroller 513 | [@Nyholm]: https://github.com/Nyholm 514 | [@NicolasBadey]: https://github.com/NicolasBadey 515 | [@NiR-]: https://github.com/NiR- 516 | [@Mondane]: https://github.com/Mondane 517 | [@MisatoTremor]: https://github.com/MisatoTremor 518 | [@MAXakaWIZARD]: https://github.com/MAXakaWIZARD 519 | [@Lusitanian]: https://github.com/Lusitanian 520 | [@JoydS]: https://github.com/JoydS 521 | [@EmmanuelVella]: https://github.com/EmmanuelVella 522 | [@Einenlum]: https://github.com/Einenlum 523 | [@DerekRoth]: https://github.com/DerekRoth 524 | [1.6.0]: https://github.com/KnpLabs/DoctrineBehaviors/compare/1.5.0...1.6.0 525 | [1.5.0]: https://github.com/KnpLabs/DoctrineBehaviors/compare/1.4.1...1.5.0 526 | [1.4.1]: https://github.com/KnpLabs/DoctrineBehaviors/compare/1.4.0...1.4.1 527 | [1.4.0]: https://github.com/KnpLabs/DoctrineBehaviors/compare/v2.0.0-alpha4...1.4.0 528 | [#478]: https://github.com/KnpLabs/DoctrineBehaviors/pull/478 529 | [#477]: https://github.com/KnpLabs/DoctrineBehaviors/pull/477 530 | [#475]: https://github.com/KnpLabs/DoctrineBehaviors/pull/475 531 | [#474]: https://github.com/KnpLabs/DoctrineBehaviors/pull/474 532 | [#473]: https://github.com/KnpLabs/DoctrineBehaviors/pull/473 533 | [#472]: https://github.com/KnpLabs/DoctrineBehaviors/pull/472 534 | [#470]: https://github.com/KnpLabs/DoctrineBehaviors/pull/470 535 | [v2.0.0-beta1]: https://github.com/KnpLabs/DoctrineBehaviors/compare/v2.0.0-alpha4...v2.0.0-beta1 536 | [v2.0.0-alpha1]: https://github.com/KnpLabs/DoctrineBehaviors/compare/1.6.0...v2.0.0-alpha1 537 | [@hermann8u]: https://github.com/hermann8u 538 | [@StanislavUngr]: https://github.com/StanislavUngr 539 | [#480]: https://github.com/KnpLabs/DoctrineBehaviors/pull/480 540 | [#479]: https://github.com/KnpLabs/DoctrineBehaviors/pull/479 541 | [v2.0.0]: https://github.com/KnpLabs/DoctrineBehaviors/compare/v2.0.0-beta1...v2.0.0 542 | [#493]: https://github.com/KnpLabs/DoctrineBehaviors/pull/493 543 | [#490]: https://github.com/KnpLabs/DoctrineBehaviors/pull/490 544 | [#487]: https://github.com/KnpLabs/DoctrineBehaviors/pull/487 545 | [#484]: https://github.com/KnpLabs/DoctrineBehaviors/pull/484 546 | [v2.0.1]: https://github.com/KnpLabs/DoctrineBehaviors/compare/v2.0.0...v2.0.1 547 | [@toooni]: https://github.com/toooni 548 | [@ckrack]: https://github.com/ckrack 549 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License 2 | --------------- 3 | 4 | Copyright (c) KnpLabs 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is furnished 11 | to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Doctrine Behaviors 2 | 3 | [![Downloads](https://img.shields.io/packagist/dt/knplabs/doctrine-behaviors.svg?style=flat-square)](https://packagist.org/packages/knplabs/doctrine-behaviors) 4 | 5 | This PHP library is a collection of traits and interfaces that add behaviors to Doctrine entities and repositories. 6 | 7 | It currently handles: 8 | 9 | * [Blameable](/docs/blameable.md) 10 | * [Loggable](/docs/loggable.md) 11 | * [Sluggable](/docs/sluggable.md) 12 | * [SoftDeletable](/docs/soft-deletable.md) 13 | * [Uuidable](/docs/uuidable.md) 14 | * [Timestampable](/docs/timestampable.md) 15 | * [Translatable](/docs/translatable.md) 16 | * [Tree](/docs/tree.md) 17 | 18 | ## Install 19 | 20 | ```bash 21 | composer require knplabs/doctrine-behaviors 22 | ``` 23 | 24 | ## Usage 25 | 26 | All you have to do is to define a Doctrine entity: 27 | 28 | - implemented interface 29 | - add a trait 30 | 31 | For some behaviors like tree, you can use repository traits: 32 | 33 | ```php 34 | import(DoctrineSetList::DOCTRINE_BEHAVIORS_20); 103 | }; 104 | ``` 105 | 106 | Run Rector: 107 | 108 | ```bash 109 | vendor/bin/rector process src 110 | ``` 111 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "knplabs/doctrine-behaviors", 3 | "description": "Doctrine Behavior Traits", 4 | "type": "symfony-bundle", 5 | "keywords": [ 6 | "behaviors", "doctrine", "timestampable", "translatable", "blameable", "softdeletable", "tree", "uuid" 7 | ], 8 | "homepage": "http://knplabs.com", 9 | "license": "MIT", 10 | "authors": [ 11 | { "name": "Knplabs", "homepage": "http://knplabs.com" } 12 | ], 13 | "require": { 14 | "php": ">=8.0", 15 | "doctrine/common": "^3.3", 16 | "doctrine/persistence": "^2.5|^3.0", 17 | "doctrine/dbal": "^3.3", 18 | "doctrine/orm": "^2.12", 19 | "doctrine/doctrine-bundle": "^2.6", 20 | "symfony/cache": "^5.4|^6.0", 21 | "symfony/dependency-injection": "^5.4|^6.0", 22 | "symfony/http-kernel": "^5.4|^6.0", 23 | "symfony/security-core": "^5.4|^6.0", 24 | "symfony/framework-bundle": "^5.4|^6.0", 25 | "symfony/string": "^5.4|^6.0", 26 | "symfony/translation-contracts": "^2.4|^3.0", 27 | "nette/utils": "^3.2", 28 | "ramsey/uuid": "^4.2" 29 | }, 30 | "require-dev": { 31 | "ext-pdo_sqlite": "*", 32 | "ext-pdo_mysql": "*", 33 | "ext-pdo_pgsql": "*", 34 | "psr/log": "^1.1", 35 | "doctrine/annotations": "^1.13", 36 | "php-parallel-lint/php-parallel-lint": "^1.3", 37 | "phpstan/phpstan": "^1.7.10", 38 | "phpunit/phpunit": "^9.5", 39 | "rector/rector": "^0.13.4", 40 | "symplify/easy-coding-standard": "^10.2.9", 41 | "symplify/phpstan-extensions": "^10.2.9", 42 | "phpstan/phpstan-doctrine": "^1.3", 43 | "phpstan/phpstan-phpunit": "^1.1", 44 | "symplify/package-builder": "^10.2.9", 45 | "symplify/phpstan-rules": "^10.2.9", 46 | "phpstan/extension-installer": "^1.1", 47 | "symplify/easy-ci": "^10.2.9" 48 | }, 49 | "autoload": { 50 | "psr-4": { 51 | "Knp\\DoctrineBehaviors\\": "src", 52 | "Knp\\DoctrineBehaviors\\PHPStan\\": "utils/phpstan-behaviors/src" 53 | } 54 | }, 55 | "autoload-dev": { 56 | "psr-4": { 57 | "Knp\\DoctrineBehaviors\\Tests\\": "tests" 58 | } 59 | }, 60 | "scripts": { 61 | "check-cs": "vendor/bin/ecs check --ansi", 62 | "fix-cs": "vendor/bin/ecs check --fix --ansi", 63 | "phpstan": "vendor/bin/phpstan analyse --ansi --error-format symplify" 64 | }, 65 | "config": { 66 | "allow-plugins": { 67 | "composer/package-versions-deprecated": true, 68 | "phpstan/extension-installer": true 69 | } 70 | }, 71 | "extra": { 72 | "phpstan": { 73 | "includes": [ 74 | "phpstan-extension.neon" 75 | ] 76 | } 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /config/services.php: -------------------------------------------------------------------------------- 1 | parameters(); 9 | 10 | $parameters->set('doctrine_behaviors_translatable_fetch_mode', 'LAZY'); 11 | $parameters->set('doctrine_behaviors_translation_fetch_mode', 'LAZY'); 12 | $parameters->set('doctrine_behaviors_blameable_user_entity', null); 13 | $parameters->set('doctrine_behaviors_timestampable_date_field_type', 'datetime'); 14 | 15 | $services = $containerConfigurator->services(); 16 | 17 | $services->defaults() 18 | ->public() 19 | ->autowire() 20 | ->autoconfigure() 21 | ->bind('$translatableFetchMode', '%doctrine_behaviors_translatable_fetch_mode%') 22 | ->bind('$translationFetchMode', '%doctrine_behaviors_translation_fetch_mode%') 23 | ->bind('$blameableUserEntity', '%doctrine_behaviors_blameable_user_entity%') 24 | ->bind('$timestampableDateFieldType', '%doctrine_behaviors_timestampable_date_field_type%'); 25 | 26 | $services->load('Knp\DoctrineBehaviors\\', __DIR__ . '/../src') 27 | ->exclude([ 28 | __DIR__ . '/../src/Bundle', 29 | __DIR__ . '/../src/DoctrineBehaviorsBundle.php', 30 | __DIR__ . '/../src/Exception', 31 | ]); 32 | }; 33 | -------------------------------------------------------------------------------- /phpstan-extension.neon: -------------------------------------------------------------------------------- 1 | services: 2 | - 3 | class: Knp\DoctrineBehaviors\PHPStan\Type\TranslatableTranslateDynamicMethodReturnTypeExtension 4 | tags: [phpstan.broker.dynamicMethodReturnTypeExtension] 5 | - 6 | class: Knp\DoctrineBehaviors\PHPStan\Type\TranslatableGetTranslationsDynamicMethodReturnTypeExtension 7 | tags: [phpstan.broker.dynamicMethodReturnTypeExtension] 8 | - 9 | class: Knp\DoctrineBehaviors\PHPStan\Type\TranslationGetTranslatableDynamicMethodReturnTypeExtension 10 | tags: [phpstan.broker.dynamicMethodReturnTypeExtension] 11 | -------------------------------------------------------------------------------- /src/Bundle/DependencyInjection/DoctrineBehaviorsExtension.php: -------------------------------------------------------------------------------- 1 | load('services.php'); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/Contract/Entity/BlameableInterface.php: -------------------------------------------------------------------------------- 1 | 13 | */ 14 | public function getTranslations(); 15 | 16 | /** 17 | * @return Collection 18 | */ 19 | public function getNewTranslations(): Collection; 20 | 21 | public function addTranslation(TranslationInterface $translation): void; 22 | 23 | public function removeTranslation(TranslationInterface $translation): void; 24 | 25 | /** 26 | * Returns translation for specific locale (creates new one if doesn't exists). If requested translation doesn't 27 | * exist, it will first try to fallback default locale If any translation doesn't exist, it will be added to 28 | * newTranslations collection. In order to persist new translations, call mergeNewTranslations method, before flush 29 | * 30 | * @param string $locale The locale (en, ru, fr) | null If null, will try with current locale 31 | */ 32 | public function translate(?string $locale = null, bool $fallbackToDefault = true): TranslationInterface; 33 | 34 | /** 35 | * Merges newly created translations into persisted translations. 36 | */ 37 | public function mergeNewTranslations(): void; 38 | 39 | public function setCurrentLocale(string $locale): void; 40 | 41 | public function getCurrentLocale(): string; 42 | 43 | public function setDefaultLocale(string $locale): void; 44 | 45 | public function getDefaultLocale(): string; 46 | 47 | public static function getTranslationEntityClass(): string; 48 | } 49 | -------------------------------------------------------------------------------- /src/Contract/Entity/TranslationInterface.php: -------------------------------------------------------------------------------- 1 | 61 | */ 62 | public function getChildNodes(): Collection; 63 | 64 | public function isLeafNode(): bool; 65 | 66 | public function isRootNode(): bool; 67 | 68 | public function getRootNode(): self; 69 | 70 | public function isChildNodeOf(self $treeNode): bool; 71 | 72 | public function getNodeLevel(): int; 73 | 74 | /** 75 | * Builds a hierarchical tree from a flat collection of NodeInterface elements 76 | * 77 | * @param self[] $treeNodes 78 | */ 79 | public function buildTree(array $treeNodes): void; 80 | } 81 | -------------------------------------------------------------------------------- /src/Contract/Entity/UuidableInterface.php: -------------------------------------------------------------------------------- 1 | getClassMetadata(); 47 | if ($classMetadata->reflClass === null) { 48 | // Class has not yet been fully built, ignore this event 49 | return; 50 | } 51 | 52 | if (! is_a($classMetadata->reflClass->getName(), BlameableInterface::class, true)) { 53 | return; 54 | } 55 | 56 | $this->mapEntity($classMetadata); 57 | } 58 | 59 | /** 60 | * Stores the current user into createdBy and updatedBy properties 61 | */ 62 | public function prePersist(LifecycleEventArgs $lifecycleEventArgs): void 63 | { 64 | $entity = $lifecycleEventArgs->getEntity(); 65 | if (! $entity instanceof BlameableInterface) { 66 | return; 67 | } 68 | 69 | $user = $this->userProvider->provideUser(); 70 | // no user set → skip 71 | if ($user === null) { 72 | return; 73 | } 74 | 75 | if (! $entity->getCreatedBy()) { 76 | $entity->setCreatedBy($user); 77 | 78 | $this->getUnitOfWork() 79 | ->propertyChanged($entity, self::CREATED_BY, null, $user); 80 | } 81 | 82 | if (! $entity->getUpdatedBy()) { 83 | $entity->setUpdatedBy($user); 84 | 85 | $this->getUnitOfWork() 86 | ->propertyChanged($entity, self::UPDATED_BY, null, $user); 87 | } 88 | } 89 | 90 | /** 91 | * Stores the current user into updatedBy property 92 | */ 93 | public function preUpdate(LifecycleEventArgs $lifecycleEventArgs): void 94 | { 95 | $entity = $lifecycleEventArgs->getEntity(); 96 | if (! $entity instanceof BlameableInterface) { 97 | return; 98 | } 99 | 100 | $user = $this->userProvider->provideUser(); 101 | if ($user === null) { 102 | return; 103 | } 104 | 105 | $oldValue = $entity->getUpdatedBy(); 106 | $entity->setUpdatedBy($user); 107 | 108 | $this->getUnitOfWork() 109 | ->propertyChanged($entity, self::UPDATED_BY, $oldValue, $user); 110 | } 111 | 112 | /** 113 | * Stores the current user into deletedBy property 114 | */ 115 | public function preRemove(LifecycleEventArgs $lifecycleEventArgs): void 116 | { 117 | $entity = $lifecycleEventArgs->getEntity(); 118 | if (! $entity instanceof BlameableInterface) { 119 | return; 120 | } 121 | 122 | $user = $this->userProvider->provideUser(); 123 | if ($user === null) { 124 | return; 125 | } 126 | 127 | $oldDeletedBy = $entity->getDeletedBy(); 128 | $entity->setDeletedBy($user); 129 | 130 | $this->getUnitOfWork() 131 | ->propertyChanged($entity, self::DELETED_BY, $oldDeletedBy, $user); 132 | } 133 | 134 | /** 135 | * @return string[] 136 | */ 137 | public function getSubscribedEvents(): array 138 | { 139 | return [Events::prePersist, Events::preUpdate, Events::preRemove, Events::loadClassMetadata]; 140 | } 141 | 142 | private function mapEntity(ClassMetadataInfo $classMetadataInfo): void 143 | { 144 | if ($this->blameableUserEntity !== null && class_exists($this->blameableUserEntity)) { 145 | $this->mapManyToOneUser($classMetadataInfo); 146 | } else { 147 | $this->mapStringUser($classMetadataInfo); 148 | } 149 | } 150 | 151 | private function getUnitOfWork(): UnitOfWork 152 | { 153 | return $this->entityManager->getUnitOfWork(); 154 | } 155 | 156 | private function mapManyToOneUser(ClassMetadataInfo $classMetadataInfo): void 157 | { 158 | $this->mapManyToOneWithTargetEntity($classMetadataInfo, self::CREATED_BY); 159 | $this->mapManyToOneWithTargetEntity($classMetadataInfo, self::UPDATED_BY); 160 | $this->mapManyToOneWithTargetEntity($classMetadataInfo, self::DELETED_BY); 161 | } 162 | 163 | private function mapStringUser(ClassMetadataInfo $classMetadataInfo): void 164 | { 165 | $this->mapStringNullableField($classMetadataInfo, self::CREATED_BY); 166 | $this->mapStringNullableField($classMetadataInfo, self::UPDATED_BY); 167 | $this->mapStringNullableField($classMetadataInfo, self::DELETED_BY); 168 | } 169 | 170 | private function mapManyToOneWithTargetEntity(ClassMetadataInfo $classMetadataInfo, string $fieldName): void 171 | { 172 | if ($classMetadataInfo->hasAssociation($fieldName)) { 173 | return; 174 | } 175 | 176 | $classMetadataInfo->mapManyToOne([ 177 | 'fieldName' => $fieldName, 178 | 'targetEntity' => $this->blameableUserEntity, 179 | 'joinColumns' => [ 180 | [ 181 | 'onDelete' => 'SET NULL', 182 | ], 183 | ], 184 | ]); 185 | } 186 | 187 | private function mapStringNullableField(ClassMetadataInfo $classMetadataInfo, string $fieldName): void 188 | { 189 | if ($classMetadataInfo->hasField($fieldName)) { 190 | return; 191 | } 192 | 193 | $classMetadataInfo->mapField([ 194 | 'fieldName' => $fieldName, 195 | 'type' => 'string', 196 | 'nullable' => true, 197 | ]); 198 | } 199 | } 200 | -------------------------------------------------------------------------------- /src/EventSubscriber/LoggableEventSubscriber.php: -------------------------------------------------------------------------------- 1 | getEntity(); 24 | if (! $entity instanceof LoggableInterface) { 25 | return; 26 | } 27 | 28 | $createLogMessage = $entity->getCreateLogMessage(); 29 | $this->logger->log(LogLevel::INFO, $createLogMessage); 30 | 31 | $this->logChangeSet($lifecycleEventArgs); 32 | } 33 | 34 | public function postUpdate(LifecycleEventArgs $lifecycleEventArgs): void 35 | { 36 | $entity = $lifecycleEventArgs->getEntity(); 37 | if (! $entity instanceof LoggableInterface) { 38 | return; 39 | } 40 | 41 | $this->logChangeSet($lifecycleEventArgs); 42 | } 43 | 44 | public function preRemove(LifecycleEventArgs $lifecycleEventArgs): void 45 | { 46 | $entity = $lifecycleEventArgs->getEntity(); 47 | 48 | if ($entity instanceof LoggableInterface) { 49 | $this->logger->log(LogLevel::INFO, $entity->getRemoveLogMessage()); 50 | } 51 | } 52 | 53 | /** 54 | * @return string[] 55 | */ 56 | public function getSubscribedEvents(): array 57 | { 58 | return [Events::postPersist, Events::postUpdate, Events::preRemove]; 59 | } 60 | 61 | /** 62 | * Logs entity changeset 63 | */ 64 | private function logChangeSet(LifecycleEventArgs $lifecycleEventArgs): void 65 | { 66 | $entityManager = $lifecycleEventArgs->getEntityManager(); 67 | $unitOfWork = $entityManager->getUnitOfWork(); 68 | $entity = $lifecycleEventArgs->getEntity(); 69 | 70 | $entityClass = $entity::class; 71 | $classMetadata = $entityManager->getClassMetadata($entityClass); 72 | 73 | /** @var LoggableInterface $entity */ 74 | $unitOfWork->computeChangeSet($classMetadata, $entity); 75 | $changeSet = $unitOfWork->getEntityChangeSet($entity); 76 | 77 | $message = $entity->getUpdateLogMessage($changeSet); 78 | 79 | if ($message === '') { 80 | return; 81 | } 82 | 83 | $this->logger->log(LogLevel::INFO, $message); 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/EventSubscriber/SluggableEventSubscriber.php: -------------------------------------------------------------------------------- 1 | getClassMetadata(); 32 | if ($this->shouldSkip($classMetadata)) { 33 | return; 34 | } 35 | 36 | $classMetadata->mapField([ 37 | 'fieldName' => self::SLUG, 38 | 'type' => 'string', 39 | 'nullable' => true, 40 | ]); 41 | } 42 | 43 | public function prePersist(LifecycleEventArgs $lifecycleEventArgs): void 44 | { 45 | $this->processLifecycleEventArgs($lifecycleEventArgs); 46 | } 47 | 48 | public function preUpdate(LifecycleEventArgs $lifecycleEventArgs): void 49 | { 50 | $this->processLifecycleEventArgs($lifecycleEventArgs); 51 | } 52 | 53 | /** 54 | * @return string[] 55 | */ 56 | public function getSubscribedEvents(): array 57 | { 58 | return [Events::loadClassMetadata, Events::prePersist, Events::preUpdate]; 59 | } 60 | 61 | private function shouldSkip(ClassMetadataInfo $classMetadataInfo): bool 62 | { 63 | if (! is_a($classMetadataInfo->getName(), SluggableInterface::class, true)) { 64 | return true; 65 | } 66 | 67 | return $classMetadataInfo->hasField(self::SLUG); 68 | } 69 | 70 | private function processLifecycleEventArgs(LifecycleEventArgs $lifecycleEventArgs): void 71 | { 72 | $entity = $lifecycleEventArgs->getEntity(); 73 | if (! $entity instanceof SluggableInterface) { 74 | return; 75 | } 76 | 77 | $entity->generateSlug(); 78 | 79 | if ($entity->shouldGenerateUniqueSlugs()) { 80 | $this->generateUniqueSlugFor($entity); 81 | } 82 | } 83 | 84 | private function generateUniqueSlugFor(SluggableInterface $sluggable): void 85 | { 86 | $i = 0; 87 | $slug = $sluggable->getSlug(); 88 | 89 | $uniqueSlug = $slug; 90 | 91 | while (! ( 92 | $this->defaultSluggableRepository->isSlugUniqueFor($sluggable, $uniqueSlug) 93 | && $this->isSlugUniqueInUnitOfWork($sluggable, $uniqueSlug) 94 | )) { 95 | $uniqueSlug = $slug . '-' . ++$i; 96 | } 97 | 98 | $sluggable->setSlug($uniqueSlug); 99 | } 100 | 101 | private function isSlugUniqueInUnitOfWork(SluggableInterface $sluggable, string $uniqueSlug): bool 102 | { 103 | $scheduledEntities = $this->getOtherScheduledEntities($sluggable); 104 | foreach ($scheduledEntities as $scheduledEntity) { 105 | if ($scheduledEntity->getSlug() === $uniqueSlug) { 106 | return false; 107 | } 108 | } 109 | 110 | return true; 111 | } 112 | 113 | /** 114 | * @return SluggableInterface[] 115 | */ 116 | private function getOtherScheduledEntities(SluggableInterface $sluggable): array 117 | { 118 | $unitOfWork = $this->entityManager->getUnitOfWork(); 119 | 120 | $uowScheduledEntities = [ 121 | ...$unitOfWork->getScheduledEntityInsertions(), 122 | ...$unitOfWork->getScheduledEntityUpdates(), 123 | ...$unitOfWork->getScheduledEntityDeletions(), 124 | ]; 125 | 126 | $scheduledEntities = []; 127 | foreach ($uowScheduledEntities as $uowScheduledEntity) { 128 | if ($uowScheduledEntity instanceof SluggableInterface && $sluggable !== $uowScheduledEntity) { 129 | $scheduledEntities[] = $uowScheduledEntity; 130 | } 131 | } 132 | 133 | return $scheduledEntities; 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /src/EventSubscriber/SoftDeletableEventSubscriber.php: -------------------------------------------------------------------------------- 1 | getEntityManager(); 23 | $unitOfWork = $entityManager->getUnitOfWork(); 24 | 25 | foreach ($unitOfWork->getScheduledEntityDeletions() as $entity) { 26 | if (! $entity instanceof SoftDeletableInterface) { 27 | continue; 28 | } 29 | 30 | $oldValue = $entity->getDeletedAt(); 31 | 32 | $entity->delete(); 33 | $entityManager->persist($entity); 34 | 35 | $unitOfWork->propertyChanged($entity, self::DELETED_AT, $oldValue, $entity->getDeletedAt()); 36 | $unitOfWork->scheduleExtraUpdate($entity, [ 37 | self::DELETED_AT => [$oldValue, $entity->getDeletedAt()], 38 | ]); 39 | } 40 | } 41 | 42 | public function loadClassMetadata(LoadClassMetadataEventArgs $loadClassMetadataEventArgs): void 43 | { 44 | $classMetadata = $loadClassMetadataEventArgs->getClassMetadata(); 45 | if ($classMetadata->reflClass === null) { 46 | // Class has not yet been fully built, ignore this event 47 | return; 48 | } 49 | 50 | if (! is_a($classMetadata->reflClass->getName(), SoftDeletableInterface::class, true)) { 51 | return; 52 | } 53 | 54 | if ($classMetadata->hasField(self::DELETED_AT)) { 55 | return; 56 | } 57 | 58 | $classMetadata->mapField([ 59 | 'fieldName' => self::DELETED_AT, 60 | 'type' => 'datetime', 61 | 'nullable' => true, 62 | ]); 63 | } 64 | 65 | /** 66 | * @return string[] 67 | */ 68 | public function getSubscribedEvents(): array 69 | { 70 | return [Events::onFlush, Events::loadClassMetadata]; 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/EventSubscriber/TimestampableEventSubscriber.php: -------------------------------------------------------------------------------- 1 | getClassMetadata(); 22 | if ($classMetadata->reflClass === null) { 23 | // Class has not yet been fully built, ignore this event 24 | return; 25 | } 26 | 27 | if (! is_a($classMetadata->reflClass->getName(), TimestampableInterface::class, true)) { 28 | return; 29 | } 30 | 31 | if ($classMetadata->isMappedSuperclass) { 32 | return; 33 | } 34 | 35 | $classMetadata->addLifecycleCallback('updateTimestamps', Events::prePersist); 36 | $classMetadata->addLifecycleCallback('updateTimestamps', Events::preUpdate); 37 | 38 | foreach (['createdAt', 'updatedAt'] as $field) { 39 | if (! $classMetadata->hasField($field)) { 40 | $classMetadata->mapField([ 41 | 'fieldName' => $field, 42 | 'type' => $this->timestampableDateFieldType, 43 | 'nullable' => true, 44 | ]); 45 | } 46 | } 47 | } 48 | 49 | /** 50 | * @return string[] 51 | */ 52 | public function getSubscribedEvents(): array 53 | { 54 | return [Events::loadClassMetadata]; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/EventSubscriber/TranslatableEventSubscriber.php: -------------------------------------------------------------------------------- 1 | translatableFetchMode = $this->convertFetchString($translatableFetchMode); 35 | $this->translationFetchMode = $this->convertFetchString($translationFetchMode); 36 | } 37 | 38 | /** 39 | * Adds mapping to the translatable and translations. 40 | */ 41 | public function loadClassMetadata(LoadClassMetadataEventArgs $loadClassMetadataEventArgs): void 42 | { 43 | $classMetadata = $loadClassMetadataEventArgs->getClassMetadata(); 44 | if (! $classMetadata->reflClass instanceof ReflectionClass) { 45 | // Class has not yet been fully built, ignore this event 46 | return; 47 | } 48 | 49 | if ($classMetadata->isMappedSuperclass) { 50 | return; 51 | } 52 | 53 | if (is_a($classMetadata->reflClass->getName(), TranslatableInterface::class, true)) { 54 | $this->mapTranslatable($classMetadata); 55 | } 56 | 57 | if (is_a($classMetadata->reflClass->getName(), TranslationInterface::class, true)) { 58 | $this->mapTranslation($classMetadata, $loadClassMetadataEventArgs->getObjectManager()); 59 | } 60 | } 61 | 62 | public function postLoad(LifecycleEventArgs $lifecycleEventArgs): void 63 | { 64 | $this->setLocales($lifecycleEventArgs); 65 | } 66 | 67 | public function prePersist(LifecycleEventArgs $lifecycleEventArgs): void 68 | { 69 | $this->setLocales($lifecycleEventArgs); 70 | } 71 | 72 | /** 73 | * @return string[] 74 | */ 75 | public function getSubscribedEvents(): array 76 | { 77 | return [Events::loadClassMetadata, Events::postLoad, Events::prePersist]; 78 | } 79 | 80 | /** 81 | * Convert string FETCH mode to required string 82 | */ 83 | private function convertFetchString(string|int $fetchMode): int 84 | { 85 | if (is_int($fetchMode)) { 86 | return $fetchMode; 87 | } 88 | 89 | if ($fetchMode === 'EAGER') { 90 | return ClassMetadataInfo::FETCH_EAGER; 91 | } 92 | 93 | if ($fetchMode === 'EXTRA_LAZY') { 94 | return ClassMetadataInfo::FETCH_EXTRA_LAZY; 95 | } 96 | 97 | return ClassMetadataInfo::FETCH_LAZY; 98 | } 99 | 100 | private function mapTranslatable(ClassMetadataInfo $classMetadataInfo): void 101 | { 102 | if ($classMetadataInfo->hasAssociation('translations')) { 103 | return; 104 | } 105 | 106 | $classMetadataInfo->mapOneToMany([ 107 | 'fieldName' => 'translations', 108 | 'mappedBy' => 'translatable', 109 | 'indexBy' => self::LOCALE, 110 | 'cascade' => ['persist', 'merge', 'remove'], 111 | 'fetch' => $this->translatableFetchMode, 112 | 'targetEntity' => $classMetadataInfo->getReflectionClass() 113 | ->getMethod('getTranslationEntityClass') 114 | ->invoke(null), 115 | 'orphanRemoval' => true, 116 | ]); 117 | } 118 | 119 | private function mapTranslation(ClassMetadataInfo $classMetadataInfo, ObjectManager $objectManager): void 120 | { 121 | if (! $classMetadataInfo->hasAssociation('translatable')) { 122 | $targetEntity = $classMetadataInfo->getReflectionClass() 123 | ->getMethod('getTranslatableEntityClass') 124 | ->invoke(null); 125 | 126 | /** @var ClassMetadataInfo $classMetadata */ 127 | $classMetadata = $objectManager->getClassMetadata($targetEntity); 128 | 129 | $singleIdentifierFieldName = $classMetadata->getSingleIdentifierFieldName(); 130 | 131 | $classMetadataInfo->mapManyToOne([ 132 | 'fieldName' => 'translatable', 133 | 'inversedBy' => 'translations', 134 | 'cascade' => ['persist', 'merge'], 135 | 'fetch' => $this->translationFetchMode, 136 | 'joinColumns' => [[ 137 | 'name' => 'translatable_id', 138 | 'referencedColumnName' => $singleIdentifierFieldName, 139 | 'onDelete' => 'CASCADE', 140 | ]], 141 | 'targetEntity' => $targetEntity, 142 | ]); 143 | } 144 | 145 | $name = $classMetadataInfo->getTableName() . '_unique_translation'; 146 | if (! $this->hasUniqueTranslationConstraint($classMetadataInfo, $name) && 147 | $classMetadataInfo->getName() === $classMetadataInfo->rootEntityName) { 148 | $classMetadataInfo->table['uniqueConstraints'][$name] = [ 149 | 'columns' => ['translatable_id', self::LOCALE], 150 | ]; 151 | } 152 | 153 | if (! $classMetadataInfo->hasField(self::LOCALE) && ! $classMetadataInfo->hasAssociation(self::LOCALE)) { 154 | $classMetadataInfo->mapField([ 155 | 'fieldName' => self::LOCALE, 156 | 'type' => 'string', 157 | 'length' => 5, 158 | ]); 159 | } 160 | } 161 | 162 | private function setLocales(LifecycleEventArgs $lifecycleEventArgs): void 163 | { 164 | $entity = $lifecycleEventArgs->getEntity(); 165 | if (! $entity instanceof TranslatableInterface) { 166 | return; 167 | } 168 | 169 | $currentLocale = $this->localeProvider->provideCurrentLocale(); 170 | if ($currentLocale) { 171 | $entity->setCurrentLocale($currentLocale); 172 | } 173 | 174 | $fallbackLocale = $this->localeProvider->provideFallbackLocale(); 175 | if ($fallbackLocale) { 176 | $entity->setDefaultLocale($fallbackLocale); 177 | } 178 | } 179 | 180 | private function hasUniqueTranslationConstraint(ClassMetadataInfo $classMetadataInfo, string $name): bool 181 | { 182 | return isset($classMetadataInfo->table['uniqueConstraints'][$name]); 183 | } 184 | } 185 | -------------------------------------------------------------------------------- /src/EventSubscriber/TreeEventSubscriber.php: -------------------------------------------------------------------------------- 1 | getClassMetadata(); 17 | if ($classMetadata->reflClass === null) { 18 | // Class has not yet been fully built, ignore this event 19 | return; 20 | } 21 | 22 | if (! is_a($classMetadata->reflClass->getName(), TreeNodeInterface::class, true)) { 23 | return; 24 | } 25 | 26 | if ($classMetadata->hasField('materializedPath')) { 27 | return; 28 | } 29 | 30 | $classMetadata->mapField([ 31 | 'fieldName' => 'materializedPath', 32 | 'type' => 'string', 33 | 'length' => 255, 34 | ]); 35 | } 36 | 37 | /** 38 | * @return string[] 39 | */ 40 | public function getSubscribedEvents(): array 41 | { 42 | return [Events::loadClassMetadata]; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/EventSubscriber/UuidableEventSubscriber.php: -------------------------------------------------------------------------------- 1 | getClassMetadata(); 18 | if ($classMetadata->reflClass === null) { 19 | // Class has not yet been fully built, ignore this event 20 | return; 21 | } 22 | 23 | if (! is_a($classMetadata->reflClass->getName(), UuidableInterface::class, true)) { 24 | return; 25 | } 26 | 27 | if ($classMetadata->hasField('uuid')) { 28 | return; 29 | } 30 | 31 | $classMetadata->mapField([ 32 | 'fieldName' => 'uuid', 33 | 'type' => 'string', 34 | 'nullable' => true, 35 | ]); 36 | } 37 | 38 | public function prePersist(LifecycleEventArgs $lifecycleEventArgs): void 39 | { 40 | $entity = $lifecycleEventArgs->getEntity(); 41 | if (! $entity instanceof UuidableInterface) { 42 | return; 43 | } 44 | 45 | $entity->generateUuid(); 46 | } 47 | 48 | /** 49 | * @return string[] 50 | */ 51 | public function getSubscribedEvents(): array 52 | { 53 | return [Events::loadClassMetadata, Events::prePersist]; 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/Exception/ShouldNotHappenException.php: -------------------------------------------------------------------------------- 1 | createdBy = $user; 15 | } 16 | 17 | /** 18 | * @param string|int|object $user 19 | */ 20 | public function setUpdatedBy($user): void 21 | { 22 | $this->updatedBy = $user; 23 | } 24 | 25 | /** 26 | * @param string|int|object $user 27 | */ 28 | public function setDeletedBy($user): void 29 | { 30 | $this->deletedBy = $user; 31 | } 32 | 33 | /** 34 | * @return int|object|string 35 | */ 36 | public function getCreatedBy() 37 | { 38 | return $this->createdBy; 39 | } 40 | 41 | /** 42 | * @return int|object|string 43 | */ 44 | public function getUpdatedBy() 45 | { 46 | return $this->updatedBy; 47 | } 48 | 49 | /** 50 | * @return string|int|object|null 51 | */ 52 | public function getDeletedBy() 53 | { 54 | return $this->deletedBy; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/Model/Blameable/BlameablePropertiesTrait.php: -------------------------------------------------------------------------------- 1 | $changeSet) { 15 | $itemCount = count($changeSet); 16 | 17 | for ($i = 0, $s = $itemCount; $i < $s; ++$i) { 18 | $item = $changeSet[$i]; 19 | 20 | if ($item instanceof DateTime) { 21 | $changeSet[$i] = $item->format('Y-m-d H:i:s.u'); 22 | } 23 | } 24 | 25 | if ($changeSet[0] === $changeSet[1]) { 26 | continue; 27 | } 28 | 29 | $message[] = $this->createChangeSetMessage($property, $changeSet); 30 | } 31 | 32 | return implode("\n", $message); 33 | } 34 | 35 | public function getCreateLogMessage(): string 36 | { 37 | return sprintf('%s #%s created', self::class, $this->getId()); 38 | } 39 | 40 | public function getRemoveLogMessage(): string 41 | { 42 | return sprintf('%s #%s removed', self::class, $this->getId()); 43 | } 44 | 45 | private function createChangeSetMessage(string $property, array $changeSet): string 46 | { 47 | return sprintf( 48 | '%s #%s : property "%s" changed from "%s" to "%s"', 49 | self::class, 50 | $this->getId(), 51 | $property, 52 | is_array($changeSet[0]) ? 'an array' : (string) $changeSet[0], 53 | is_array($changeSet[1]) ? 'an array' : (string) $changeSet[1] 54 | ); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/Model/Sluggable/SluggableMethodsTrait.php: -------------------------------------------------------------------------------- 1 | slug = $slug; 15 | } 16 | 17 | public function getSlug(): string 18 | { 19 | return $this->slug; 20 | } 21 | 22 | /** 23 | * Generates and sets the entity's slug. Called prePersist and preUpdate 24 | */ 25 | public function generateSlug(): void 26 | { 27 | if ($this->slug !== null && $this->shouldRegenerateSlugOnUpdate() === false) { 28 | return; 29 | } 30 | 31 | $values = []; 32 | foreach ($this->getSluggableFields() as $sluggableField) { 33 | $values[] = $this->resolveFieldValue($sluggableField); 34 | } 35 | 36 | $this->slug = $this->generateSlugValue($values); 37 | } 38 | 39 | public function shouldGenerateUniqueSlugs(): bool 40 | { 41 | return false; 42 | } 43 | 44 | private function getSlugDelimiter(): string 45 | { 46 | return '-'; 47 | } 48 | 49 | private function shouldRegenerateSlugOnUpdate(): bool 50 | { 51 | return true; 52 | } 53 | 54 | private function generateSlugValue(array $values): string 55 | { 56 | $usableValues = []; 57 | foreach ($values as $value) { 58 | if (! empty($value)) { 59 | $usableValues[] = $value; 60 | } 61 | } 62 | 63 | $this->ensureAtLeastOneUsableValue($values, $usableValues); 64 | 65 | // generate the slug itself 66 | $sluggableText = implode(' ', $usableValues); 67 | 68 | $unicodeString = (new AsciiSlugger())->slug($sluggableText, $this->getSlugDelimiter()); 69 | 70 | return strtolower($unicodeString->toString()); 71 | } 72 | 73 | private function ensureAtLeastOneUsableValue(array $values, array $usableValues): void 74 | { 75 | if (count($usableValues) >= 1) { 76 | return; 77 | } 78 | 79 | throw new SluggableException(sprintf( 80 | 'Sluggable expects to have at least one non-empty field from the following: ["%s"]', 81 | implode('", "', array_keys($values)) 82 | )); 83 | } 84 | 85 | /** 86 | * @return mixed|null 87 | */ 88 | private function resolveFieldValue(string $field) 89 | { 90 | if (property_exists($this, $field)) { 91 | return $this->{$field}; 92 | } 93 | 94 | $methodName = 'get' . ucfirst($field); 95 | if (method_exists($this, $methodName)) { 96 | return $this->{$methodName}(); 97 | } 98 | 99 | return null; 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /src/Model/Sluggable/SluggablePropertiesTrait.php: -------------------------------------------------------------------------------- 1 | deletedAt = $this->currentDateTime(); 17 | } 18 | 19 | /** 20 | * Restore entity by undeleting it 21 | */ 22 | public function restore(): void 23 | { 24 | $this->deletedAt = null; 25 | } 26 | 27 | public function isDeleted(): bool 28 | { 29 | if ($this->deletedAt !== null) { 30 | return $this->deletedAt <= $this->currentDateTime(); 31 | } 32 | 33 | return false; 34 | } 35 | 36 | public function willBeDeleted(?DateTimeInterface $deletedAt = null): bool 37 | { 38 | if ($this->deletedAt === null) { 39 | return false; 40 | } 41 | 42 | if ($deletedAt === null) { 43 | return true; 44 | } 45 | 46 | return $this->deletedAt <= $deletedAt; 47 | } 48 | 49 | public function getDeletedAt(): ?DateTimeInterface 50 | { 51 | return $this->deletedAt; 52 | } 53 | 54 | public function setDeletedAt(?DateTimeInterface $deletedAt): void 55 | { 56 | $this->deletedAt = $deletedAt; 57 | } 58 | 59 | private function currentDateTime(): DateTimeInterface 60 | { 61 | $dateTime = DateTime::createFromFormat('U.u', sprintf('%.6F', microtime(true))); 62 | if ($dateTime === false) { 63 | throw new ShouldNotHappenException(); 64 | } 65 | 66 | $dateTime->setTimezone(new DateTimeZone(date_default_timezone_get())); 67 | 68 | return $dateTime; 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/Model/SoftDeletable/SoftDeletablePropertiesTrait.php: -------------------------------------------------------------------------------- 1 | createdAt; 17 | } 18 | 19 | public function getUpdatedAt(): ?DateTimeInterface 20 | { 21 | return $this->updatedAt; 22 | } 23 | 24 | public function setCreatedAt(DateTimeInterface $createdAt): void 25 | { 26 | $this->createdAt = $createdAt; 27 | } 28 | 29 | public function setUpdatedAt(DateTimeInterface $updatedAt): void 30 | { 31 | $this->updatedAt = $updatedAt; 32 | } 33 | 34 | /** 35 | * Updates createdAt and updatedAt timestamps. 36 | */ 37 | public function updateTimestamps(): void 38 | { 39 | // Create a datetime with microseconds 40 | $dateTime = DateTime::createFromFormat('U.u', sprintf('%.6F', microtime(true))); 41 | 42 | if ($dateTime === false) { 43 | throw new ShouldNotHappenException(); 44 | } 45 | 46 | $dateTime->setTimezone(new DateTimeZone(date_default_timezone_get())); 47 | 48 | if ($this->createdAt === null) { 49 | $this->createdAt = $dateTime; 50 | } 51 | 52 | $this->updatedAt = $dateTime; 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/Model/Timestampable/TimestampablePropertiesTrait.php: -------------------------------------------------------------------------------- 1 | 16 | */ 17 | public function getTranslations() 18 | { 19 | // initialize collection, usually in ctor 20 | if ($this->translations === null) { 21 | $this->translations = new ArrayCollection(); 22 | } 23 | 24 | return $this->translations; 25 | } 26 | 27 | /** 28 | * @param Collection $translations 29 | * @phpstan-param iterable $translations 30 | */ 31 | public function setTranslations(iterable $translations): void 32 | { 33 | $this->ensureIsIterableOrCollection($translations); 34 | 35 | foreach ($translations as $translation) { 36 | $this->addTranslation($translation); 37 | } 38 | } 39 | 40 | /** 41 | * @return Collection 42 | */ 43 | public function getNewTranslations(): Collection 44 | { 45 | // initialize collection, usually in ctor 46 | if ($this->newTranslations === null) { 47 | $this->newTranslations = new ArrayCollection(); 48 | } 49 | 50 | return $this->newTranslations; 51 | } 52 | 53 | public function addTranslation(TranslationInterface $translation): void 54 | { 55 | $this->getTranslations() 56 | ->set($translation->getLocale(), $translation); 57 | $translation->setTranslatable($this); 58 | } 59 | 60 | public function removeTranslation(TranslationInterface $translation): void 61 | { 62 | $this->getTranslations() 63 | ->removeElement($translation); 64 | } 65 | 66 | /** 67 | * Returns translation for specific locale (creates new one if doesn't exists). If requested translation doesn't 68 | * exist, it will first try to fallback default locale If any translation doesn't exist, it will be added to 69 | * newTranslations collection. In order to persist new translations, call mergeNewTranslations method, before flush 70 | * 71 | * @param string $locale The locale (en, ru, fr) | null If null, will try with current locale 72 | */ 73 | public function translate(?string $locale = null, bool $fallbackToDefault = true): TranslationInterface 74 | { 75 | return $this->doTranslate($locale, $fallbackToDefault); 76 | } 77 | 78 | /** 79 | * Merges newly created translations into persisted translations. 80 | */ 81 | public function mergeNewTranslations(): void 82 | { 83 | foreach ($this->getNewTranslations() as $newTranslation) { 84 | if (! $this->getTranslations()->contains($newTranslation) && ! $newTranslation->isEmpty()) { 85 | $this->addTranslation($newTranslation); 86 | $this->getNewTranslations() 87 | ->removeElement($newTranslation); 88 | } 89 | } 90 | 91 | foreach ($this->getTranslations() as $translation) { 92 | if (! $translation->isEmpty()) { 93 | continue; 94 | } 95 | 96 | $this->removeTranslation($translation); 97 | } 98 | } 99 | 100 | public function setCurrentLocale(string $locale): void 101 | { 102 | $this->currentLocale = $locale; 103 | } 104 | 105 | public function getCurrentLocale(): string 106 | { 107 | return $this->currentLocale ?: $this->getDefaultLocale(); 108 | } 109 | 110 | public function setDefaultLocale(string $locale): void 111 | { 112 | $this->defaultLocale = $locale; 113 | } 114 | 115 | public function getDefaultLocale(): string 116 | { 117 | return $this->defaultLocale; 118 | } 119 | 120 | public static function getTranslationEntityClass(): string 121 | { 122 | return static::class . 'Translation'; 123 | } 124 | 125 | /** 126 | * Returns translation for specific locale (creates new one if doesn't exists). If requested translation doesn't 127 | * exist, it will first try to fallback default locale If any translation doesn't exist, it will be added to 128 | * newTranslations collection. In order to persist new translations, call mergeNewTranslations method, before flush 129 | * 130 | * @param string $locale The locale (en, ru, fr) | null If null, will try with current locale 131 | */ 132 | protected function doTranslate(?string $locale = null, bool $fallbackToDefault = true): TranslationInterface 133 | { 134 | if ($locale === null) { 135 | $locale = $this->getCurrentLocale(); 136 | } 137 | 138 | $foundTranslation = $this->findTranslationByLocale($locale); 139 | if ($foundTranslation && ! $foundTranslation->isEmpty()) { 140 | return $foundTranslation; 141 | } 142 | 143 | if ($fallbackToDefault) { 144 | $fallbackTranslation = $this->resolveFallbackTranslation($locale); 145 | if ($fallbackTranslation !== null) { 146 | return $fallbackTranslation; 147 | } 148 | } 149 | 150 | if ($foundTranslation) { 151 | return $foundTranslation; 152 | } 153 | 154 | $translationEntityClass = static::getTranslationEntityClass(); 155 | 156 | /** @var TranslationInterface $translation */ 157 | $translation = new $translationEntityClass(); 158 | $translation->setLocale($locale); 159 | 160 | $this->getNewTranslations() 161 | ->set($translation->getLocale(), $translation); 162 | $translation->setTranslatable($this); 163 | 164 | return $translation; 165 | } 166 | 167 | /** 168 | * An extra feature allows you to proxy translated fields of a translatable entity. 169 | * 170 | * @return mixed The translated value of the field for current locale 171 | */ 172 | protected function proxyCurrentLocaleTranslation(string $method, array $arguments = []) 173 | { 174 | // allow $entity->name call $entity->getName() in templates 175 | if (! method_exists(self::getTranslationEntityClass(), $method)) { 176 | $method = 'get' . ucfirst($method); 177 | } 178 | 179 | $translation = $this->translate($this->getCurrentLocale()); 180 | 181 | return call_user_func_array([$translation, $method], $arguments); 182 | } 183 | 184 | /** 185 | * Finds specific translation in collection by its locale. 186 | */ 187 | protected function findTranslationByLocale(string $locale, bool $withNewTranslations = true): ?TranslationInterface 188 | { 189 | $translation = $this->getTranslations() 190 | ->get($locale); 191 | 192 | if ($translation) { 193 | return $translation; 194 | } 195 | 196 | if ($withNewTranslations) { 197 | return $this->getNewTranslations() 198 | ->get($locale); 199 | } 200 | 201 | return null; 202 | } 203 | 204 | protected function computeFallbackLocale(string $locale): ?string 205 | { 206 | if (strrchr($locale, '_') !== false) { 207 | return substr($locale, 0, -strlen(strrchr($locale, '_'))); 208 | } 209 | 210 | return null; 211 | } 212 | 213 | /** 214 | * @param Collection|mixed $translations 215 | */ 216 | private function ensureIsIterableOrCollection($translations): void 217 | { 218 | if ($translations instanceof Collection) { 219 | return; 220 | } 221 | 222 | if (is_iterable($translations)) { 223 | return; 224 | } 225 | 226 | throw new TranslatableException( 227 | sprintf('$translations parameter must be iterable or %s', Collection::class) 228 | ); 229 | } 230 | 231 | private function resolveFallbackTranslation(string $locale): ?TranslationInterface 232 | { 233 | $fallbackLocale = $this->computeFallbackLocale($locale); 234 | 235 | if ($fallbackLocale !== null) { 236 | $translation = $this->findTranslationByLocale($fallbackLocale); 237 | if ($translation && ! $translation->isEmpty()) { 238 | return $translation; 239 | } 240 | } 241 | 242 | return $this->findTranslationByLocale($this->getDefaultLocale(), false); 243 | } 244 | } 245 | -------------------------------------------------------------------------------- /src/Model/Translatable/TranslatablePropertiesTrait.php: -------------------------------------------------------------------------------- 1 | 14 | */ 15 | protected $translations; 16 | 17 | /** 18 | * @see mergeNewTranslations 19 | * @var Collection 20 | */ 21 | protected $newTranslations; 22 | 23 | /** 24 | * currentLocale is a non persisted field configured during postLoad event 25 | * 26 | * @var string|null 27 | */ 28 | protected $currentLocale; 29 | 30 | /** 31 | * @var string 32 | */ 33 | protected $defaultLocale = 'en'; 34 | } 35 | -------------------------------------------------------------------------------- /src/Model/Translatable/TranslatableTrait.php: -------------------------------------------------------------------------------- 1 | translatable = $translatable; 24 | } 25 | 26 | /** 27 | * Returns entity, that this translation is mapped to. 28 | */ 29 | public function getTranslatable(): TranslatableInterface 30 | { 31 | return $this->translatable; 32 | } 33 | 34 | public function setLocale(string $locale): void 35 | { 36 | $this->locale = $locale; 37 | } 38 | 39 | public function getLocale(): string 40 | { 41 | return $this->locale; 42 | } 43 | 44 | public function isEmpty(): bool 45 | { 46 | foreach (get_object_vars($this) as $var => $value) { 47 | if (in_array($var, ['id', 'translatable', 'locale'], true)) { 48 | continue; 49 | } 50 | 51 | if (is_string($value) && strlen(trim($value)) > 0) { 52 | return false; 53 | } 54 | 55 | if (! empty($value)) { 56 | return false; 57 | } 58 | } 59 | 60 | return true; 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/Model/Translatable/TranslationPropertiesTrait.php: -------------------------------------------------------------------------------- 1 | getId(); 23 | } 24 | 25 | public static function getMaterializedPathSeparator(): string 26 | { 27 | return '/'; 28 | } 29 | 30 | public function getRealMaterializedPath(): string 31 | { 32 | if ($this->getMaterializedPath() === self::getMaterializedPathSeparator()) { 33 | return $this->getMaterializedPath() . $this->getNodeId(); 34 | } 35 | 36 | return $this->getMaterializedPath() . self::getMaterializedPathSeparator() . $this->getNodeId(); 37 | } 38 | 39 | public function getMaterializedPath(): string 40 | { 41 | return $this->materializedPath; 42 | } 43 | 44 | public function setMaterializedPath(string $path): void 45 | { 46 | $this->materializedPath = $path; 47 | $this->setParentMaterializedPath($this->getParentMaterializedPath()); 48 | } 49 | 50 | public function getParentMaterializedPath(): string 51 | { 52 | $path = $this->getExplodedPath(); 53 | array_pop($path); 54 | 55 | return static::getMaterializedPathSeparator() . implode(static::getMaterializedPathSeparator(), $path); 56 | } 57 | 58 | public function setParentMaterializedPath(string $path): void 59 | { 60 | $this->parentNodePath = $path; 61 | } 62 | 63 | public function getRootMaterializedPath(): string 64 | { 65 | $explodedPath = $this->getExplodedPath(); 66 | 67 | return static::getMaterializedPathSeparator() . array_shift($explodedPath); 68 | } 69 | 70 | public function getNodeLevel(): int 71 | { 72 | return count($this->getExplodedPath()); 73 | } 74 | 75 | public function isRootNode(): bool 76 | { 77 | return self::getMaterializedPathSeparator() === $this->getParentMaterializedPath(); 78 | } 79 | 80 | public function isLeafNode(): bool 81 | { 82 | return $this->getChildNodes() 83 | ->count() === 0; 84 | } 85 | 86 | /** 87 | * @return Collection 88 | */ 89 | public function getChildNodes(): Collection 90 | { 91 | // set default value as in entity constructors 92 | if ($this->childNodes === null) { 93 | $this->childNodes = new ArrayCollection(); 94 | } 95 | 96 | return $this->childNodes; 97 | } 98 | 99 | public function addChildNode(TreeNodeInterface $treeNode): void 100 | { 101 | $this->getChildNodes() 102 | ->add($treeNode); 103 | } 104 | 105 | public function isIndirectChildNodeOf(TreeNodeInterface $treeNode): bool 106 | { 107 | return $this->getRealMaterializedPath() !== $treeNode->getRealMaterializedPath() 108 | && str_starts_with($this->getRealMaterializedPath(), $treeNode->getRealMaterializedPath()); 109 | } 110 | 111 | public function isChildNodeOf(TreeNodeInterface $treeNode): bool 112 | { 113 | return $this->getParentMaterializedPath() === $treeNode->getRealMaterializedPath(); 114 | } 115 | 116 | public function setChildNodeOf(?TreeNodeInterface $treeNode = null): void 117 | { 118 | $id = $this->getNodeId(); 119 | if ($id === '' || $id === null) { 120 | throw new TreeException('You must provide an id for this node if you want it to be part of a tree.'); 121 | } 122 | 123 | $path = $treeNode !== null 124 | ? rtrim($treeNode->getRealMaterializedPath(), static::getMaterializedPathSeparator()) 125 | : static::getMaterializedPathSeparator(); 126 | $this->setMaterializedPath($path); 127 | 128 | if ($this->parentNode !== null) { 129 | $this->parentNode->getChildNodes() 130 | ->removeElement($this); 131 | } 132 | 133 | $this->parentNode = $treeNode; 134 | 135 | if ($treeNode !== null) { 136 | $this->parentNode->addChildNode($this); 137 | } 138 | 139 | foreach ($this->getChildNodes() as $childNode) { 140 | /** @var TreeNodeInterface $this */ 141 | $childNode->setChildNodeOf($this); 142 | } 143 | } 144 | 145 | public function getParentNode(): ?TreeNodeInterface 146 | { 147 | return $this->parentNode; 148 | } 149 | 150 | public function setParentNode(TreeNodeInterface $treeNode): void 151 | { 152 | $this->parentNode = $treeNode; 153 | $this->setChildNodeOf($this->parentNode); 154 | } 155 | 156 | public function getRootNode(): TreeNodeInterface 157 | { 158 | $parent = $this; 159 | while ($parent->getParentNode() !== null) { 160 | $parent = $parent->getParentNode(); 161 | } 162 | 163 | return $parent; 164 | } 165 | 166 | /** 167 | * @param TreeNodeInterface[] $treeNodes 168 | */ 169 | public function buildTree(array $treeNodes): void 170 | { 171 | $this->getChildNodes() 172 | ->clear(); 173 | 174 | foreach ($treeNodes as $treeNode) { 175 | if ($treeNode->getMaterializedPath() !== $this->getRealMaterializedPath()) { 176 | continue; 177 | } 178 | 179 | $treeNode->setParentNode($this); 180 | $treeNode->buildTree($treeNodes); 181 | } 182 | } 183 | 184 | /** 185 | * @param Closure $prepare a function to prepare the node before putting into the result 186 | */ 187 | public function toJson(?Closure $prepare = null): string 188 | { 189 | $tree = $this->toArray($prepare); 190 | 191 | return Json::encode($tree); 192 | } 193 | 194 | /** 195 | * @param Closure $prepare a function to prepare the node before putting into the result 196 | */ 197 | public function toArray(?Closure $prepare = null, ?array &$tree = null): array 198 | { 199 | if ($prepare === null) { 200 | $prepare = static fn (TreeNodeInterface $node): string => (string) $node; 201 | } 202 | 203 | if ($tree === null) { 204 | $tree = [ 205 | $this->getNodeId() => [ 206 | /** @var TreeNodeInterface $this */ 207 | 'node' => $prepare($this), 208 | 'children' => [], 209 | ], 210 | ]; 211 | } 212 | 213 | foreach ($this->getChildNodes() as $childNode) { 214 | $tree[$this->getNodeId()]['children'][$childNode->getNodeId()] = [ 215 | 'node' => $prepare($childNode), 216 | 'children' => [], 217 | ]; 218 | 219 | $childNode->toArray($prepare, $tree[$this->getNodeId()]['children']); 220 | } 221 | 222 | return $tree; 223 | } 224 | 225 | /** 226 | * @param Closure $prepare a function to prepare the node before putting into the result 227 | * @param array $tree a reference to an array, used internally for recursion 228 | */ 229 | public function toFlatArray(?Closure $prepare = null, ?array &$tree = null): array 230 | { 231 | if ($prepare === null) { 232 | $prepare = static function (TreeNodeInterface $treeNode) { 233 | $pre = $treeNode->getNodeLevel() > 1 ? implode('', array_fill(0, $treeNode->getNodeLevel(), '--')) : ''; 234 | return $pre . $treeNode; 235 | }; 236 | } 237 | 238 | if ($tree === null) { 239 | $tree = [ 240 | $this->getNodeId() => $prepare($this), 241 | ]; 242 | } 243 | 244 | foreach ($this->getChildNodes() as $childNode) { 245 | $tree[$childNode->getNodeId()] = $prepare($childNode); 246 | $childNode->toFlatArray($prepare, $tree); 247 | } 248 | 249 | return $tree; 250 | } 251 | 252 | /** 253 | * @param TreeNodeInterface $node 254 | */ 255 | public function offsetSet(mixed $offset, $node): void 256 | { 257 | /** @var TreeNodeInterface $this */ 258 | $node->setChildNodeOf($this); 259 | } 260 | 261 | public function offsetExists(mixed $offset): bool 262 | { 263 | return isset($this->getChildNodes()[$offset]); 264 | } 265 | 266 | public function offsetUnset(mixed $offset): void 267 | { 268 | unset($this->getChildNodes()[$offset]); 269 | } 270 | 271 | /** 272 | * @return mixed 273 | */ 274 | public function offsetGet(mixed $offset) 275 | { 276 | return $this->getChildNodes()[$offset]; 277 | } 278 | 279 | /** 280 | * @return string[] 281 | */ 282 | protected function getExplodedPath(): array 283 | { 284 | $separator = static::getMaterializedPathSeparator(); 285 | if ($separator === '') { 286 | throw new ShouldNotHappenException(); 287 | } 288 | 289 | $path = explode($separator, $this->getRealMaterializedPath()); 290 | 291 | return array_filter($path, static fn ($item): bool => $item !== ''); 292 | } 293 | } 294 | -------------------------------------------------------------------------------- /src/Model/Tree/TreeNodePropertiesTrait.php: -------------------------------------------------------------------------------- 1 | uuid = $uuid; 16 | } 17 | 18 | public function getUuid(): ?UuidInterface 19 | { 20 | if (is_string($this->uuid)) { 21 | if ($this->uuid === '') { 22 | throw new ShouldNotHappenException(); 23 | } 24 | 25 | return Uuid::fromString($this->uuid); 26 | } 27 | 28 | return $this->uuid; 29 | } 30 | 31 | public function generateUuid(): void 32 | { 33 | if ($this->uuid) { 34 | return; 35 | } 36 | 37 | $this->uuid = Uuid::uuid4(); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/Model/Uuidable/UuidablePropertiesTrait.php: -------------------------------------------------------------------------------- 1 | createQueryBuilder($rootAlias) 19 | ->andWhere($rootAlias . '.materializedPath = :empty') 20 | ->setParameter('empty', ''); 21 | } 22 | 23 | public function getRootNodes(string $rootAlias = 't'): array 24 | { 25 | return $this->getRootNodesQB($rootAlias) 26 | ->getQuery() 27 | ->execute(); 28 | } 29 | 30 | /** 31 | * Returns a node hydrated with its children and parents 32 | * 33 | * @return TreeNodeInterface[]|ArrayAccess|null 34 | */ 35 | public function getTree(string $path = '', string $rootAlias = 't', array $extraParams = []) 36 | { 37 | $results = $this->getFlatTree($path, $rootAlias, $extraParams); 38 | 39 | return $this->buildTree($results); 40 | } 41 | 42 | public function getTreeExceptNodeAndItsChildrenQB( 43 | TreeNodeInterface $treeNode, 44 | string $rootAlias = 't' 45 | ): QueryBuilder { 46 | return $this->getFlatTreeQB('', $rootAlias) 47 | ->andWhere($rootAlias . '.materializedPath NOT LIKE :except_path') 48 | ->andWhere($rootAlias . '.id != :id') 49 | ->setParameter('except_path', $treeNode->getRealMaterializedPath() . '%') 50 | ->setParameter('id', $treeNode->getId()); 51 | } 52 | 53 | /** 54 | * Extracts the root node and constructs a tree using flat resultset 55 | * 56 | * @return ArrayAccess|TreeNodeInterface[]|null 57 | */ 58 | public function buildTree(array $results) 59 | { 60 | if ($results === []) { 61 | return null; 62 | } 63 | 64 | $root = $results[0]; 65 | $root->buildTree($results); 66 | 67 | return $root; 68 | } 69 | 70 | /** 71 | * Constructs a query builder to get a flat tree, starting from a given path 72 | */ 73 | public function getFlatTreeQB(string $path = '', string $rootAlias = 't', array $extraParams = []): QueryBuilder 74 | { 75 | $queryBuilder = $this->createQueryBuilder($rootAlias) 76 | ->andWhere($rootAlias . '.materializedPath LIKE :path') 77 | ->addOrderBy($rootAlias . '.materializedPath', 'ASC') 78 | ->setParameter('path', $path . '%'); 79 | 80 | $parentId = basename($path); 81 | if ($parentId !== '' && $parentId !== '0') { 82 | $queryBuilder->orWhere($rootAlias . '.id = :parent') 83 | ->setParameter('parent', $parentId); 84 | } 85 | 86 | $this->addFlatTreeConditions($queryBuilder, $extraParams); 87 | 88 | return $queryBuilder; 89 | } 90 | 91 | /** 92 | * @param mixed[] $extraParams 93 | * @return mixed[] 94 | */ 95 | public function getFlatTree(string $path, string $rootAlias = 't', array $extraParams = []): array 96 | { 97 | return $this->getFlatTreeQB($path, $rootAlias, $extraParams) 98 | ->getQuery() 99 | ->execute(); 100 | } 101 | 102 | /** 103 | * Manipulates the flat tree query builder before executing it. Override this method to customize the tree query 104 | */ 105 | protected function addFlatTreeConditions(QueryBuilder $queryBuilder, array $extraParams): void 106 | { 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /src/Provider/LocaleProvider.php: -------------------------------------------------------------------------------- 1 | requestStack->getCurrentRequest(); 27 | if (! $currentRequest instanceof Request) { 28 | return null; 29 | } 30 | 31 | $currentLocale = $currentRequest->getLocale(); 32 | if ($currentLocale !== '') { 33 | return $currentLocale; 34 | } 35 | 36 | if ($this->translator !== null) { 37 | return $this->translator->getLocale(); 38 | } 39 | 40 | return null; 41 | } 42 | 43 | public function provideFallbackLocale(): ?string 44 | { 45 | $currentRequest = $this->requestStack->getCurrentRequest(); 46 | if ($currentRequest !== null) { 47 | return $currentRequest->getDefaultLocale(); 48 | } 49 | 50 | try { 51 | if ($this->parameterBag->has('locale')) { 52 | return (string) $this->parameterBag->get('locale'); 53 | } 54 | 55 | return (string) $this->parameterBag->get('kernel.default_locale'); 56 | } catch (ParameterNotFoundException | InvalidArgumentException) { 57 | return null; 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/Provider/UserProvider.php: -------------------------------------------------------------------------------- 1 | security->getToken(); 21 | if ($token !== null) { 22 | $user = $token->getUser(); 23 | if ($this->blameableUserEntity) { 24 | if ($user instanceof $this->blameableUserEntity) { 25 | return $user; 26 | } 27 | } else { 28 | return $user; 29 | } 30 | } 31 | 32 | return null; 33 | } 34 | 35 | public function provideUserEntity(): ?string 36 | { 37 | $user = $this->provideUser(); 38 | if ($user === null) { 39 | return null; 40 | } 41 | 42 | if (is_object($user)) { 43 | return $user::class; 44 | } 45 | 46 | return null; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/Repository/DefaultSluggableRepository.php: -------------------------------------------------------------------------------- 1 | entityManager->createQueryBuilder() 22 | ->select('COUNT(e)') 23 | ->from($entityClass, 'e') 24 | ->andWhere('e.slug = :slug') 25 | ->setParameter('slug', $uniqueSlug); 26 | 27 | $identifiers = $this->entityManager->getClassMetadata($entityClass) 28 | ->getIdentifierValues($sluggable); 29 | 30 | foreach ($identifiers as $field => $value) { 31 | if ($value === null || $field === 'slug') { 32 | continue; 33 | } 34 | 35 | $normalizedField = \str_replace('.', '_', $field); 36 | 37 | $queryBuilder 38 | ->andWhere(\sprintf('e.%s != :%s', $field, $normalizedField)) 39 | ->setParameter($normalizedField, $value); 40 | } 41 | 42 | return ! (bool) $queryBuilder->getQuery() 43 | ->getSingleScalarResult(); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /utils/phpstan-behaviors/src/Exception/PHPStanTypeException.php: -------------------------------------------------------------------------------- 1 | getType($methodCall->var); 23 | /** @var class-string $translatableClass */ 24 | $translatableClass = $type->getReferencedClasses()[0]; 25 | 26 | if (! $reflectionProvider->hasClass($translatableClass)) { 27 | // for some reason, we the reflectin provided cannot locate the class 28 | $reflectionClass = new ReflectionClass($translatableClass); 29 | } else { 30 | $reflectionClass = $reflectionProvider->getClass($translatableClass) 31 | ->getNativeReflection(); 32 | } 33 | 34 | if ($reflectionClass->isInterface()) { 35 | if ($reflectionClass->getName() === TranslatableInterface::class || $reflectionClass->implementsInterface( 36 | TranslatableInterface::class 37 | )) { 38 | return TranslationInterface::class; 39 | } 40 | 41 | $errorMessage = sprintf( 42 | 'Unable to find the Translation class associated to the Translatable class "%s".', 43 | $reflectionClass->getName() 44 | ); 45 | throw new PHPStanTypeException($errorMessage); 46 | } 47 | 48 | return $reflectionClass 49 | ->getMethod('getTranslationEntityClass') 50 | ->invoke(null); 51 | } 52 | 53 | public static function getTranslatableClass( 54 | ReflectionProvider $reflectionProvider, 55 | MethodCall $methodCall, 56 | Scope $scope 57 | ): string { 58 | $type = $scope->getType($methodCall->var); 59 | $translationClass = $type->getReferencedClasses()[0]; 60 | $nativeReflection = $reflectionProvider->getClass($translationClass) 61 | ->getNativeReflection(); 62 | 63 | if ($nativeReflection->isInterface()) { 64 | if ($nativeReflection->getName() === TranslationInterface::class || $nativeReflection->implementsInterface( 65 | TranslationInterface::class 66 | )) { 67 | return TranslatableInterface::class; 68 | } 69 | 70 | $errorMessage = sprintf( 71 | 'Unable to find the Translatable class associated to the Translation class "%s".', 72 | $nativeReflection->getName() 73 | ); 74 | throw new PHPStanTypeException($errorMessage); 75 | } 76 | 77 | return $nativeReflection 78 | ->getMethod('getTranslatableEntityClass') 79 | ->invoke(null); 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /utils/phpstan-behaviors/src/Type/TranslatableGetTranslationsDynamicMethodReturnTypeExtension.php: -------------------------------------------------------------------------------- 1 | getName(), ['getTranslations', 'getNewTranslations'], true); 36 | } 37 | 38 | public function getTypeFromMethodCall( 39 | MethodReflection $methodReflection, 40 | MethodCall $methodCall, 41 | Scope $scope 42 | ): Type { 43 | $translationClass = StaticTranslationTypeHelper::getTranslationClass( 44 | $this->reflectionProvider, 45 | $methodCall, 46 | $scope 47 | ); 48 | 49 | return TypeCombinator::intersect( 50 | new ObjectType(Collection::class), 51 | new IterableType(new MixedType(), new ObjectType($translationClass)) 52 | ); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /utils/phpstan-behaviors/src/Type/TranslatableTranslateDynamicMethodReturnTypeExtension.php: -------------------------------------------------------------------------------- 1 | getName() === 'translate'; 31 | } 32 | 33 | public function getTypeFromMethodCall( 34 | MethodReflection $methodReflection, 35 | MethodCall $methodCall, 36 | Scope $scope 37 | ): Type { 38 | $translationClass = StaticTranslationTypeHelper::getTranslationClass( 39 | $this->reflectionProvider, 40 | $methodCall, 41 | $scope 42 | ); 43 | 44 | return new ObjectType($translationClass); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /utils/phpstan-behaviors/src/Type/TranslationGetTranslatableDynamicMethodReturnTypeExtension.php: -------------------------------------------------------------------------------- 1 | getName() === 'getTranslatable'; 31 | } 32 | 33 | public function getTypeFromMethodCall( 34 | MethodReflection $methodReflection, 35 | MethodCall $methodCall, 36 | Scope $scope 37 | ): Type { 38 | $translatableClass = StaticTranslationTypeHelper::getTranslatableClass( 39 | $this->reflectionProvider, 40 | $methodCall, 41 | $scope 42 | ); 43 | 44 | return new ObjectType($translatableClass); 45 | } 46 | } 47 | --------------------------------------------------------------------------------