Skip to content

Commit

Permalink
feat: introduce ObjectFactory
Browse files Browse the repository at this point in the history
  • Loading branch information
nikophil committed Jan 7, 2024
1 parent 76fcf21 commit b832659
Show file tree
Hide file tree
Showing 12 changed files with 220 additions and 181 deletions.
2 changes: 1 addition & 1 deletion UPGRADE-2.0.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ Here is the full list of modifications needed:

`Zenstruck\Foundry\ModelFactory` is now deprecated.
You should choose between:
- `\Zenstruck\Foundry\Object\ObjectFactory`: creates not-persistent plain objects,
- `\Zenstruck\Foundry\ObjectFactory`: creates not-persistent plain objects,
- `\Zenstruck\Foundry\Persistence\PersistentObjectFactory`: creates and stores persisted objects, and directly return them,
- `\Zenstruck\Foundry\Persistence\PersistentProxyObjectFactory`: same as above, but returns a "proxy" version of the object.
This last class basically acts the same way as the old `ModelFactory`.
Expand Down
17 changes: 6 additions & 11 deletions phpstan-baseline.neon
Original file line number Diff line number Diff line change
Expand Up @@ -51,29 +51,24 @@ parameters:
path: src/Instantiator.php

-
message: "#^Class Zenstruck\\\\Foundry\\\\Object\\\\ObjectFactory not found\\.$#"
count: 2
path: src/ModelFactory.php

-
message: "#^Method Zenstruck\\\\Foundry\\\\Persistence\\\\PersistentObjectFactory\\:\\:__callStatic\\(\\) should return array\\<int, TModel of object\\> but returns array\\<int, \\(TModel of object\\)\\|Zenstruck\\\\Foundry\\\\Persistence\\\\Proxy\\<TModel of object\\>\\>\\.$#"
message: "#^Method Zenstruck\\\\Foundry\\\\ObjectFactory\\:\\:__callStatic\\(\\) should return array\\<int, TModel of object\\> but returns array\\<int, \\(TModel of object\\)\\|Zenstruck\\\\Foundry\\\\Persistence\\\\Proxy\\<TModel of object\\>\\>\\.$#"
count: 1
path: src/Persistence/PersistentObjectFactory.php
path: src/ObjectFactory.php

-
message: "#^Method Zenstruck\\\\Foundry\\\\Persistence\\\\PersistentObjectFactory\\:\\:createSequence\\(\\) should return array\\<int, TModel of object\\> but returns array\\<int, \\(TModel of object\\)\\|Zenstruck\\\\Foundry\\\\Persistence\\\\Proxy\\<TModel of object\\>\\>\\.$#"
message: "#^Method Zenstruck\\\\Foundry\\\\ObjectFactory\\:\\:createSequence\\(\\) should return array\\<int, TModel of object\\> but returns array\\<int, \\(TModel of object\\)\\|Zenstruck\\\\Foundry\\\\Persistence\\\\Proxy\\<TModel of object\\>\\>\\.$#"
count: 1
path: src/Persistence/PersistentObjectFactory.php
path: src/ObjectFactory.php

-
message: "#^Should not use function \"debug_backtrace\", please change the code\\.$#"
count: 1
path: src/Persistence/PersistentObjectFactory.php
path: src/ObjectFactory.php

-
message: "#^Unsafe usage of new static\\(\\)\\.$#"
count: 1
path: src/Persistence/PersistentObjectFactory.php
path: src/ObjectFactory.php

-
message: "#^Should not use function \"debug_backtrace\", please change the code\\.$#"
Expand Down
37 changes: 31 additions & 6 deletions src/Factory.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
use Faker;
use Zenstruck\Foundry\Exception\FoundryBootException;
use Zenstruck\Foundry\Persistence\InversedRelationshipPostPersistCallback;
use Zenstruck\Foundry\Persistence\PersistentObjectFactory;
use Zenstruck\Foundry\Persistence\PersistentProxyObjectFactory;
use Zenstruck\Foundry\Persistence\PostPersistCallback;
use Zenstruck\Foundry\Persistence\Proxy;
Expand Down Expand Up @@ -152,7 +153,7 @@ public function create(
$callback($object, $attributes);
}

if (!$this->isPersisting()) {
if (!$this->isPersisting(calledInternally: true)) {
return $noProxy ? $object : new ProxyObject($object);
}

Expand Down Expand Up @@ -214,8 +215,18 @@ final public function sequence(iterable|callable $sequence): FactoryCollection
/**
* @return static
*/
public function withoutPersisting(): self
public function withoutPersisting(
/**
* @internal
* @deprecated
*/
bool $calledInternally = false
): self
{
if (!$calledInternally && !$this instanceof PersistentObjectFactory) {
trigger_deprecation('zenstruck\foundry', '1.37.0', 'Calling "withoutPersisting()" on a non-persistent factory class is deprecated and will trigger an error in 2.0.', __METHOD__);
}

$cloned = clone $this;
$cloned->persist = false;

Expand Down Expand Up @@ -277,6 +288,10 @@ final public function afterInstantiate(callable $callback): self
*/
final public function afterPersist(callable $callback): self
{
if (!$this instanceof PersistentObjectFactory) {
trigger_deprecation('zenstruck\foundry', '1.37.0', 'Calling "afterPersist()" on a non-persistent factory class is deprecated and will trigger an error in 2.0.', __METHOD__);
}

$cloned = clone $this;
$cloned->afterPersist[] = $callback;

Expand Down Expand Up @@ -381,8 +396,18 @@ public function shouldUseProxy(): bool
return $this instanceof PersistentProxyObjectFactory;
}

protected function isPersisting(): bool
protected function isPersisting(
/**
* @internal
* @deprecated
*/
bool $calledInternally = false
): bool
{
if (!$calledInternally && !$this instanceof PersistentObjectFactory) {
trigger_deprecation('zenstruck\foundry', '1.37.0', 'Calling "isPersisting()" on a non-persistent factory class is deprecated and will trigger an error in 2.0.', __METHOD__);
}

if (!$this->persist || !self::configuration()->isPersistEnabled() || !self::configuration()->hasManagerRegistry()) {
return false;
}
Expand Down Expand Up @@ -433,9 +458,9 @@ private function normalizeAttribute(mixed $value, string $name): mixed
return \is_object($value) ? self::normalizeObject($value) : $value;
}

if (!$this->isPersisting()) {
if (!$this->isPersisting(calledInternally: true)) {
// ensure attribute Factories' are also not persisted
$value = $value->withoutPersisting();
$value = $value->withoutPersisting(calledInternally: true);
}

if (!self::configuration()->hasManagerRegistry()) {
Expand Down Expand Up @@ -467,7 +492,7 @@ private function normalizeAttribute(mixed $value, string $name): mixed
$relationshipField = $relationshipMetadata['inversedField'];
$cascadePersist = $relationshipMetadata['cascade'];

if ($this->isPersisting() && null !== $relationshipField && false === $cascadePersist) {
if ($this->isPersisting(calledInternally: true) && null !== $relationshipField && false === $cascadePersist) {
return new InversedRelationshipPostPersistCallback($value, $relationshipField, $isCollection);
}
}
Expand Down
2 changes: 1 addition & 1 deletion src/ModelFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@

namespace Zenstruck\Foundry;

use Zenstruck\Foundry\Object\ObjectFactory;
use Zenstruck\Foundry\ObjectFactory;
use Zenstruck\Foundry\Persistence\PersistentObjectFactory;
use Zenstruck\Foundry\Persistence\PersistentProxyObjectFactory;
use Zenstruck\Foundry\Persistence\Proxy;
Expand Down
8 changes: 4 additions & 4 deletions src/ModelFactoryManager.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@

namespace Zenstruck\Foundry;

use Zenstruck\Foundry\Persistence\PersistentObjectFactory;
use Zenstruck\Foundry\ObjectFactory;

/**
* @internal
Expand All @@ -21,16 +21,16 @@
final class ModelFactoryManager
{
/**
* @param PersistentObjectFactory[] $factories
* @param ObjectFactory[] $factories
*/
public function __construct(private iterable $factories)
{
}

/**
* @param class-string<PersistentObjectFactory> $class
* @param class-string<ObjectFactory> $class
*/
public function create(string $class): PersistentObjectFactory
public function create(string $class): ObjectFactory
{
foreach ($this->factories as $factory) {
if ($class === $factory::class) {
Expand Down
166 changes: 166 additions & 0 deletions src/ObjectFactory.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
<?php

declare(strict_types=1);

/*
* This file is part of the zenstruck/foundry package.
*
* (c) Kevin Bond <kevinbond@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Zenstruck\Foundry;

use Zenstruck\Foundry\Exception\FoundryBootException;
use Zenstruck\Foundry\Factory;
use Zenstruck\Foundry\Persistence\PersistentObjectFactory;

/**
* @template TModel of object
* @template-extends Factory<TModel>
*
* @method static TModel[] createMany(int $number, array|callable $attributes = [])
* @phpstan-method static list<TModel> createMany(int $number, array|callable $attributes = [])
*
* @author Kevin Bond <kevinbond@gmail.com>
*/
abstract class ObjectFactory extends Factory
{
public function __construct()
{
parent::__construct(static::class());
}

/**
* @phpstan-return list<TModel>
*/
public static function __callStatic(string $name, array $arguments): array
{
if ('createMany' !== $name) {
throw new \BadMethodCallException(\sprintf('Call to undefined static method "%s::%s".', static::class, $name));
}

return static::new()->many($arguments[0])->create($arguments[1] ?? [], noProxy: true);
}


/**
* @final
*
* @param array|callable|string $defaultAttributes If string, assumes state
* @param string ...$states Optionally pass default states (these must be methods on your ObjectFactory with no arguments)
*/
public static function new(array|callable|string $defaultAttributes = [], string ...$states): static
{
if (\is_string($defaultAttributes)) {
$states = \array_merge([$defaultAttributes], $states);
$defaultAttributes = [];
}

try {
$factory = self::isBooted() ? self::configuration()->factories()->create(static::class) : new static();
} catch (\ArgumentCountError $e) {
throw new \RuntimeException('Model Factories with dependencies (Model Factory services) cannot be created before foundry is booted.', 0, $e);
}

$factory = $factory
->with(static fn(): array|callable => $factory->defaults())
->with($defaultAttributes);

try {
if (!is_a(static::class, PersistentObjectFactory::class, true) || !Factory::configuration()->isPersistEnabled()) {
$factory = $factory->withoutPersisting(calledInternally: true);
}
} catch (FoundryBootException) {
}

$factory = $factory->initialize();

if (!$factory instanceof static) {
throw new \TypeError(\sprintf('"%1$s::initialize()" must return an instance of "%1$s".', static::class));
}

foreach ($states as $state) {
$factory = $factory->{$state}();
}

return $factory;
}

/**
* @final
*
* @return TModel
*/
public function create(
array|callable $attributes = [],
/**
* @deprecated
* @internal
*/
bool $noProxy = false
): object {
if (2 === \count(\func_get_args()) && !\str_starts_with(\debug_backtrace(options: \DEBUG_BACKTRACE_IGNORE_ARGS, limit: 1)[0]['class'] ?? '', 'Zenstruck\Foundry')) {
trigger_deprecation('zenstruck\foundry', '1.37.0', \sprintf('Parameter "$noProxy" of method "%s()" is deprecated and will be removed in Foundry 2.0.', __METHOD__));
}

return parent::create(
$attributes,
noProxy: true
);
}

/**
* @final
*
* A shortcut to create a single model without states.
*
* @return TModel
*/
public static function createOne(array $attributes = []): object
{
return static::new()->create($attributes, noProxy: true);
}

/**
* @final
*
* A shortcut to create multiple models, based on a sequence, without states.
*
* @param iterable<array<string, mixed>>|callable(): iterable<array<string, mixed>> $sequence
*
* @return list<TModel>
*/
public static function createSequence(iterable|callable $sequence): array
{
return static::new()->sequence($sequence)->create(noProxy: true);
}

/** @phpstan-return class-string<TModel> */
abstract public static function class(): string;

/**
* Override to add default instantiator and default afterInstantiate/afterPersist events.
*
* @return static
*/
#[\ReturnTypeWillChange]
protected function initialize()
{
return $this;
}

/**
* @deprecated use with() instead
*/
final protected function addState(array|callable $attributes = []): static
{
trigger_deprecation('zenstruck\foundry', '1.37.0', \sprintf('Method "%s()" is deprecated and will be removed in version 2.0. Use "%s::with()" instead.', __METHOD__, Factory::class));

return $this->with($attributes);
}

abstract protected function defaults(): array|callable;
}
Loading

0 comments on commit b832659

Please sign in to comment.