|
| 1 | +# Migration guide from Foundry 1.x to 2.0 |
| 2 | + |
| 3 | +Foundry 2 has changed some of its API. |
| 4 | +The global philosophy is still the same. |
| 5 | +The main change is that we've introduced a separation between "object" factories, |
| 6 | +"persistence" factories and "persistence with proxy" factories. |
| 7 | + |
| 8 | +When Foundry 1.x was "persistence first", Foundry 2 is "object first". |
| 9 | +This would allow more decoupling from the persistence layer. |
| 10 | + |
| 11 | +## How to |
| 12 | + |
| 13 | +Every modification needed for a 1.x to 2.0 migration is covered by a deprecation. |
| 14 | +You'll to upgrade to the latest 1.x version, and to activate the deprecation helper, make the tests run, and |
| 15 | +fix all the deprecations reported. |
| 16 | + |
| 17 | +Here is an example of how the deprecation helper can be activated. |
| 18 | +You should set the `SYMFONY_DEPRECATIONS_HELPER` variable in `phpunit.xml` or `.env.local` file: |
| 19 | +```shell |
| 20 | +SYMFONY_DEPRECATIONS_HELPER="max[self]=0&max[direct]=0&quiet[]=indirect&quiet[]=other" |
| 21 | +``` |
| 22 | + |
| 23 | +> [!IMPORTANT] |
| 24 | +> Some deprecations can also be sent during compilation step. |
| 25 | +> These deprecations can be displayed by using the command: `$ bin/console debug:container --deprecations` |
| 26 | +
|
| 27 | +## Rector rules |
| 28 | + |
| 29 | +In the latest 1.x version, you'll find [rector rules](https://getrector.org/) which will help with the migration path. |
| 30 | + |
| 31 | +First, you'll need `rector/rector` and `phpstan/phpstan-doctrine`: |
| 32 | +```shell |
| 33 | +composer require --dev rector/rector phpstan/phpstan-doctrine |
| 34 | +``` |
| 35 | + |
| 36 | +Then, create a `rector.php` file: |
| 37 | + |
| 38 | +```php |
| 39 | +<?php |
| 40 | + |
| 41 | +use Rector\Config\RectorConfig; |
| 42 | +use Zenstruck\Foundry\Utils\Rector\FoundrySetList; |
| 43 | + |
| 44 | +return RectorConfig::configure() |
| 45 | + ->withPaths(['tests']) // add all paths where Foundry is used |
| 46 | + ->withSets([FoundrySetList::UP_TO_FOUNDRY_2]) |
| 47 | +; |
| 48 | +``` |
| 49 | + |
| 50 | +And finally, run Rector: |
| 51 | +```shell |
| 52 | +# you can run Rector in "dry run" mode, in order to see which files will be modified |
| 53 | +vendor/bin/rector process --dry-run |
| 54 | + |
| 55 | +# actually modify files |
| 56 | +vendor/bin/rector process |
| 57 | +``` |
| 58 | + |
| 59 | +> [!IMPORTANT] |
| 60 | +> Rector rules may not totally cover all deprecations (some complex cases may not be handled) |
| 61 | +> You'd still need to run the tests with deprecation helper enabled to ensure everything is fixed |
| 62 | +> and then fix all deprecations left. |
| 63 | +
|
| 64 | +> [!TIP] |
| 65 | +> You can try to run twice these rules. Sometimes, the second run will find some difference that it could not spot on |
| 66 | +> the first run. |
| 67 | +
|
| 68 | +> [!NOTE] |
| 69 | +> Once you've finished the migration to 2.0, it is not necessary anymore to keep the Foundry's rule set in your Rector |
| 70 | +> config. |
| 71 | +
|
| 72 | +### Doctrine's mapping |
| 73 | + |
| 74 | +Rector rules need to understand your Doctrine mapping to guess which one of `PersistentProxyObjectFactory` or |
| 75 | +`ObjectFactory` it should use. |
| 76 | + |
| 77 | +If your mapping is defined in the code thanks to attributes or annotations, everything is OK. |
| 78 | +If the mapping is defined outside the code, with xml, yaml or php configuration, some extra work is needed: |
| 79 | + |
| 80 | +1. Create a `tests/object-manager.php` file which will expose your doctrine config. Here is an example: |
| 81 | +```php |
| 82 | +use App\Kernel; |
| 83 | +use Symfony\Component\Dotenv\Dotenv; |
| 84 | + |
| 85 | +require __DIR__ . '/../vendor/autoload.php'; |
| 86 | + |
| 87 | +(new Dotenv())->bootEnv(__DIR__ . '/../.env'); |
| 88 | + |
| 89 | +$kernel = new Kernel($_SERVER['APP_ENV'], (bool) $_SERVER['APP_DEBUG']); |
| 90 | +$kernel->boot(); |
| 91 | +return $kernel->getContainer()->get('doctrine')->getManager(); |
| 92 | +``` |
| 93 | + |
| 94 | +2. Provide this file path to Rector's config: |
| 95 | + |
| 96 | +```php |
| 97 | +<?php |
| 98 | + |
| 99 | +use Rector\Config\RectorConfig; |
| 100 | +use Zenstruck\Foundry\Utils\Rector\PersistenceResolver; |
| 101 | +use Zenstruck\Foundry\Utils\Rector\FoundrySetList; |
| 102 | + |
| 103 | +return static function (RectorConfig $rectorConfig): void { |
| 104 | + $rectorConfig->paths(['tests']); // add all paths where Foundry is used |
| 105 | + $rectorConfig->sets([FoundrySetList::UP_TO_FOUNDRY_2]); |
| 106 | + $rectorConfig->singleton( |
| 107 | + PersistenceResolver::class, |
| 108 | + static fn() => new PersistenceResolver(__DIR__ . '/tests/object-manager.php') |
| 109 | + ); |
| 110 | +}; |
| 111 | +``` |
| 112 | + |
| 113 | +## Known BC breaks |
| 114 | + |
| 115 | +The following error cannot be covered by Rector rules nor by the deprecation layer. |
| 116 | +``` |
| 117 | +RefreshObjectFailed: Cannot auto refresh "[Entity FQCN]" as there are unsaved changes. |
| 118 | +Be sure to call ->_save() or disable auto refreshing. |
| 119 | +``` |
| 120 | + |
| 121 | +This is an auto-refresh problem, where the "proxified" object is being accessed after modification. |
| 122 | +You'll find some [documentation](https://symfony.com/bundles/ZenstruckFoundryBundle/current/index.html#auto-refresh) |
| 123 | +about it. |
| 124 | + |
| 125 | +To mitigate this problem, you should either stop using a proxy object, or wrap the modifications in the method |
| 126 | +`->_withoutAutoRefresh()`. |
| 127 | + |
| 128 | +```diff |
| 129 | +$proxyPost = PostProxyFactory::createOne(); |
| 130 | +- $proxyPost->setTitle(); |
| 131 | +- $proxyPost->setBody(); // 💥 |
| 132 | ++$proxyPost->_withoutAutoRefresh( |
| 133 | ++ function(Post $object) { |
| 134 | ++ $proxyPost->setTitle(); |
| 135 | ++ $proxyPost->setBody(); |
| 136 | ++ } |
| 137 | ++); |
| 138 | +``` |
| 139 | + |
| 140 | + |
| 141 | +## Deprecations list |
| 142 | + |
| 143 | +Here is the full list of modifications needed: |
| 144 | + |
| 145 | +### Factory |
| 146 | + |
| 147 | +- `withAttributes()` and `addState()` are both deprecated in favor of `with()` |
| 148 | +- `sequence()` and `createSequence()` do not accept `callable` as a parameter anymore |
| 149 | + |
| 150 | +#### Change factories' base class |
| 151 | + |
| 152 | +`Zenstruck\Foundry\ModelFactory` is now deprecated. |
| 153 | +You should choose between: |
| 154 | +- `\Zenstruck\Foundry\ObjectFactory`: creates not-persistent plain objects, |
| 155 | +- `\Zenstruck\Foundry\Persistence\PersistentObjectFactory`: creates and stores persisted objects, and directly return them, |
| 156 | +- `\Zenstruck\Foundry\Persistence\PersistentProxyObjectFactory`: same as above, but returns a "proxy" version of the object. |
| 157 | + This last class basically acts the same way as the old `ModelFactory`. |
| 158 | + |
| 159 | +As a rule of thumb to help you to choose between these two new factory parent classes: |
| 160 | +- using `ObjectFactory` is straightforward: if the object cannot be persisted, you must use this one |
| 161 | +- only entities (ORM) or documents (ODM) should use `PersistentObjectFactory` or `PersistentProxyObjectFactory` |
| 162 | +- you should only use `PersistentProxyObjectFactory` if you want to leverage "auto refresh" behavior |
| 163 | + |
| 164 | +> [!WARNING] |
| 165 | +> nor `PersistentObjectFactory` or `PersistentProxyObjectFactory` should be chosen to create not persistent objects. |
| 166 | +> This will throw a deprecation in 1.x and will create an error in 2.0 |
| 167 | +
|
| 168 | +> [!IMPORTANT] |
| 169 | +> Since `PersistentObjectFactory` does not return a `Proxy` anymore, you'll have to remove all calls to `->object()` |
| 170 | +> or any other proxy method on object created by this type of factory. |
| 171 | +
|
| 172 | +> [!NOTE] |
| 173 | +> You will have to change some methods prototypes in your classes: |
| 174 | +
|
| 175 | +```php |
| 176 | +// before |
| 177 | +protected function getDefaults(): array |
| 178 | +{ |
| 179 | + // ... |
| 180 | +} |
| 181 | + |
| 182 | +// after |
| 183 | +protected function defaults(): array|callable |
| 184 | +{ |
| 185 | + // ... |
| 186 | +} |
| 187 | +``` |
| 188 | + |
| 189 | +```php |
| 190 | +// before |
| 191 | +protected static function getClass(): string |
| 192 | +{ |
| 193 | + // ... |
| 194 | +} |
| 195 | + |
| 196 | +// after |
| 197 | +public static function class(): string |
| 198 | +{ |
| 199 | + // ... |
| 200 | +} |
| 201 | +``` |
| 202 | + |
| 203 | +```php |
| 204 | +// before |
| 205 | +protected function initialize() |
| 206 | +{ |
| 207 | + // ... |
| 208 | +} |
| 209 | + |
| 210 | +// after |
| 211 | +protected function initialize(); static |
| 212 | +{ |
| 213 | + // ... |
| 214 | +} |
| 215 | +``` |
| 216 | + |
| 217 | +### Proxy |
| 218 | + |
| 219 | +Foundry 2.0 will completely change how `Proxy` system works, by leveraging Symfony's lazy proxy mechanism. |
| 220 | +`Proxy` won't be anymore a wrapper class, but a "real" proxy, meaning your objects will be of the desired class AND `Proxy` object. |
| 221 | +This implies that calling `->object()` (or, now, `_real()`) everywhere to satisfy the type system won't be needed anymore! |
| 222 | + |
| 223 | +`Proxy` class comes with deprecations as well: |
| 224 | +- replace everywhere you're type-hinting `Zenstruck\Foundry\Proxy` to the interface `Zenstruck\Foundry\Persistence\Proxy` |
| 225 | +- most of `Proxy` methods are deprecated: |
| 226 | + - `object()` -> `_real()` |
| 227 | + - `save()` -> `_save()` |
| 228 | + - `remove()` -> `_delete()` |
| 229 | + - `refresh()` -> `_refresh()` |
| 230 | + - `forceSet()` -> `_set()` |
| 231 | + - `forceGet()` -> `_get()` |
| 232 | + - `repository()` -> `_repository()` |
| 233 | + - `enableAutoRefresh()` -> `_enableAutoRefresh()` |
| 234 | + - `disableAutoRefresh()` -> `_disableAutoRefresh()` |
| 235 | + - `withoutAutoRefresh()` -> `_withoutAutoRefresh()` |
| 236 | + - `isPersisted()` is removed without any replacement |
| 237 | + - `forceSetAll()` is removed without any replacement |
| 238 | + - `assertPersisted()` is removed without any replacement |
| 239 | + - `assertNotPersisted()` is removed without any replacement |
| 240 | +- Everywhere you've type-hinted `Zenstruck\Foundry\FactoryCollection<T>` which was coming from a `PersistentProxyObjectFactory`, replace to `Zenstruck\Foundry\FactoryCollection<Proxy<T>>` |
| 241 | + |
| 242 | +### Instantiator |
| 243 | + |
| 244 | +- `Zenstruck\Foundry\Instantiator` class is deprecated in favor of `\Zenstruck\Foundry\Object\Instantiator`. You should change them everywhere. |
| 245 | +- `new Instantiator()` is deprecated: use `Instantiator::withConstructor()` or `Instantiator::withoutConstructor()` depending on your needs. |
| 246 | +- `Instantiator::alwaysForceProperties()` is deprecated in favor of `Instantiator::alwaysForce()`. Be careful of the modification of the parameter which is now a variadic. |
| 247 | +- `Instantiator::allowExtraAttributes()` is deprecated in favor of `Instantiator::allowExtra()`. Be careful of the modification of the parameter which is now a variadic. |
| 248 | +- Configuration `zenstruck_foundry.without_constructor` is deprecated in favor of `zenstruck_foundry.use_constructor` |
| 249 | + |
| 250 | +### Standalone functions |
| 251 | + |
| 252 | +- `Zenstruck\Foundry\create()` -> `Zenstruck\Foundry\Persistence\persist()` |
| 253 | +- `Zenstruck\Foundry\instantiate()` -> `Zenstruck\Foundry\object()` |
| 254 | +- `Zenstruck\Foundry\repository()` -> `Zenstruck\Foundry\Persistence\repository()` |
| 255 | +- `Zenstruck\Foundry\Factory::delayFlush()` -> `Zenstruck\Foundry\Persistence\flush_after()` |
| 256 | +- Usage of any method in `Zenstruck\Foundry\Test\TestState` should be replaced by `Zenstruck\Foundry\Test\UnitTestConfig::configure()` |
| 257 | +- `Zenstruck\Foundry\instantiate_many()` is removed without any replacement |
| 258 | +- `Zenstruck\Foundry\create_many()` is removed without any replacement |
| 259 | + |
| 260 | +### Trait `Factories` |
| 261 | +- `Factories::disablePersist()` -> `Zenstruck\Foundry\Persistence\disable_persisting()` |
| 262 | +- `Factories::enablePersist()` -> `Zenstruck\Foundry\Persistence\enable_persisting()` |
| 263 | +- both `disablePersist()` and `enable_persisting()` should not be called when Foundry is booted without Doctrine (ie: in a unit test) |
| 264 | + |
| 265 | +### Bundle configuration |
| 266 | + |
| 267 | +Here is a diff of the bundle's configuration, all configs in red should be migrated to the green ones: |
| 268 | + |
| 269 | +```diff |
| 270 | +zenstruck_foundry: |
| 271 | +- auto_refresh_proxies: null |
| 272 | + instantiator: |
| 273 | +- without_constructor: false |
| 274 | ++ use_constructor: true |
| 275 | ++ orm: |
| 276 | ++ auto_persist: true |
| 277 | ++ reset: |
| 278 | ++ connections: [default] |
| 279 | ++ entity_managers: [default] |
| 280 | ++ mode: schema |
| 281 | ++ mongo: |
| 282 | ++ auto_persist: true |
| 283 | ++ reset: |
| 284 | ++ document_managers: [default] |
| 285 | +- database_resetter: |
| 286 | +- enabled: true |
| 287 | +- orm: |
| 288 | +- connections: [] |
| 289 | +- object_managers: [] |
| 290 | +- reset_mode: schema |
| 291 | +- odm: |
| 292 | +- object_managers: [] |
| 293 | +``` |
| 294 | + |
| 295 | +### Misc. |
| 296 | +- type-hinting to `Zenstruck\Foundry\RepositoryProxy` should be replaced by `Zenstruck\Foundry\Persistence\RepositoryDecorator` |
| 297 | +- type-hinting to `Zenstruck\Foundry\RepositoryAssertions` should be replaced by `Zenstruck\Foundry\Persistence\RepositoryAssertions` |
| 298 | +- Methods in `Zenstruck\Foundry\RepositoryProxy` do not return `Proxy` anymore, but they return the actual object |
| 299 | + |
| 300 | + |
| 301 | + |
0 commit comments