diff --git a/composer.lock b/composer.lock index 75bd049..6bb80bb 100644 --- a/composer.lock +++ b/composer.lock @@ -700,11 +700,11 @@ }, { "name": "phpstan/phpstan", - "version": "2.2.16", + "version": "2.3.1", "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan/zipball/46a6d9060e5a7763adfcc21ebcb8b504ebdbcb92", - "reference": "46a6d9060e5a7763adfcc21ebcb8b504ebdbcb92", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/6155239b488e95d26d1ebd95c6b4bb294d7f09db", + "reference": "6155239b488e95d26d1ebd95c6b4bb294d7f09db", "shasum": "" }, "require": { @@ -760,20 +760,20 @@ "type": "github" } ], - "time": "2026-09-25T09:31:51+00:00" + "time": "2026-10-08T21:09:11+00:00" }, { "name": "phpstan/phpstan-nette", - "version": "2.0.12", + "version": "2.0.15", "source": { "type": "git", "url": "https://github.com/phpstan/phpstan-nette.git", - "reference": "2c104f121b9db65407112069ef4fd43799c9ba0c" + "reference": "b108419a50b08b35ae7a2bf06a4570f29963179b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan-nette/zipball/2c104f121b9db65407112069ef4fd43799c9ba0c", - "reference": "2c104f121b9db65407112069ef4fd43799c9ba0c", + "url": "https://api.github.com/repos/phpstan/phpstan-nette/zipball/b108419a50b08b35ae7a2bf06a4570f29963179b", + "reference": "b108419a50b08b35ae7a2bf06a4570f29963179b", "shasum": "" }, "require": { @@ -821,27 +821,27 @@ "description": "Nette Framework class reflection extension for PHPStan", "support": { "issues": "https://github.com/phpstan/phpstan-nette/issues", - "source": "https://github.com/phpstan/phpstan-nette/tree/2.0.12" + "source": "https://github.com/phpstan/phpstan-nette/tree/2.0.15" }, - "time": "2026-07-02T10:28:46+00:00" + "time": "2026-10-12T07:46:34+00:00" }, { "name": "phpstan/phpstan-strict-rules", - "version": "2.0.12", + "version": "2.1.0", "source": { "type": "git", "url": "https://github.com/phpstan/phpstan-strict-rules.git", - "reference": "2bc5ae19ae965663b62ac907ee6342c3903ec93b" + "reference": "24ec51589f896f048d0d36de373d993b5cf71786" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan-strict-rules/zipball/2bc5ae19ae965663b62ac907ee6342c3903ec93b", - "reference": "2bc5ae19ae965663b62ac907ee6342c3903ec93b", + "url": "https://api.github.com/repos/phpstan/phpstan-strict-rules/zipball/24ec51589f896f048d0d36de373d993b5cf71786", + "reference": "24ec51589f896f048d0d36de373d993b5cf71786", "shasum": "" }, "require": { "php": "^7.4 || ^8.0", - "phpstan/phpstan": "^2.1.52" + "phpstan/phpstan": "^2.3" }, "require-dev": { "php-parallel-lint/php-parallel-lint": "^1.2", @@ -873,9 +873,9 @@ ], "support": { "issues": "https://github.com/phpstan/phpstan-strict-rules/issues", - "source": "https://github.com/phpstan/phpstan-strict-rules/tree/2.0.12" + "source": "https://github.com/phpstan/phpstan-strict-rules/tree/2.1.0" }, - "time": "2026-07-19T07:24:06+00:00" + "time": "2026-10-01T21:30:37+00:00" }, { "name": "squizlabs/php_codesniffer", diff --git a/src/Extension.php b/src/Extension.php index 6cbfb6d..dc2a9c5 100644 --- a/src/Extension.php +++ b/src/Extension.php @@ -11,6 +11,8 @@ use Nette\DI\CompilerExtension; use Nette\DI\Definitions\ServiceDefinition; use Nette\NotImplementedException; +use Nette\Schema\Expect; +use Nette\Schema\Schema; class Extension extends CompilerExtension { @@ -20,14 +22,29 @@ class Extension extends CompilerExtension public function loadConfiguration(): void { $mapper = $this->getContainerBuilder()->addDefinition($this->prefix('mapper')); - $mapper->setFactory(Mapper::class); + $mapper->setFactory(Mapper::class, [$this->config['validation']]); + /** @phpstan-ignore foreach.nonIterable */ - foreach ($this->config as $tableName => $fieldDefinitions) { + foreach ($this->config['tables'] as $tableName => $fieldDefinitions) { /** @phpstan-ignore argument.type */ $this->registerORMService($tableName, $fieldDefinitions, $mapper); } } + public function getConfigSchema(): Schema + { + return Expect::structure([ + 'validation' => Expect::bool(false), + 'tables' => Expect::arrayOf( + Expect::structure([ + 'model' => Expect::string(), + 'service' => Expect::string(), + ])->castTo('array'), + 'string' + )->castTo('array') + ])->otherItems('mixed')->castTo('array'); + } + /** * @template TModel of Model * @phpstan-param array{ diff --git a/src/Mapper.php b/src/Mapper.php index 9fd2500..b6d437e 100644 --- a/src/Mapper.php +++ b/src/Mapper.php @@ -22,6 +22,11 @@ class Mapper */ private array $map = []; + public function __construct( + public readonly bool $validate + ) { + } + /** * @throws Throwable * @phpstan-param TKey $table diff --git a/src/Model/Model.php b/src/Model/Model.php index 7e64209..492b920 100644 --- a/src/Model/Model.php +++ b/src/Model/Model.php @@ -53,17 +53,16 @@ public function &__get(string $key): mixed //phpcs:ignore if (!is_null($value) && isset($docs[$key])) { $item = $docs[$key]; if ($item['type']->isClass()) { - $returnType = $item['reflection']; - if (isset($returnType)) { - if ($value instanceof ActiveRow) { - if ($returnType->isSubclassOf(self::class)) { - $value = $returnType->newInstance($value->toArray(), $value->getTable()); - } - } elseif ($returnType->isSubclassOf(\BackedEnum::class)) { - $value = $returnType->getMethod('tryFrom')->invoke($returnType, $value); - } elseif ($returnType->name === WGS84Point::class) { - $value = $returnType->getMethod('fromBytes')->invoke($returnType, $value); + /** @phpstan-ignore argument.type */ + $returnType = new \ReflectionClass($item['type']->getSingleName()); + if ($value instanceof ActiveRow) { + if ($returnType->isSubclassOf(self::class)) { + $value = $returnType->newInstance($value->toArray(), $value->getTable()); } + } elseif ($returnType->isSubclassOf(\BackedEnum::class)) { + $value = $returnType->getMethod('tryFrom')->invoke($returnType, $value); + } elseif ($returnType->name === WGS84Point::class) { + $value = $returnType->getMethod('fromBytes')->invoke($returnType, $value); } } } @@ -92,9 +91,9 @@ public function getReferencedModel(string $requestedModel): ?self if ($path !== null) { foreach ($path as $item) { $nextModel = $item['type'] === 'property' - /** @phpstan-ignore property.dynamicName */ + /** @phpstan-ignore property.dynamicName */ ? $newModel->{$item['accessor']} - /** @phpstan-ignore method.dynamicName */ + /** @phpstan-ignore method.dynamicName */ : $newModel->{$item['accessor']}(); if (is_null($nextModel)) { if ($item['nullable']) { diff --git a/src/ModelRelationsParser.php b/src/ModelRelationsParser.php index 0aea722..0c4de18 100644 --- a/src/ModelRelationsParser.php +++ b/src/ModelRelationsParser.php @@ -12,17 +12,18 @@ use ReflectionException; use ReflectionNamedType; +/** + * @phpstan-type ColumnDoc array{ + * type:Type, + * property:string + * } + */ class ModelRelationsParser { /** * @template TModel of Model * @phpstan-param ReflectionClass $modelReflection - * @phpstan-return array|null, - * property:string - * }>|null - * @throws ReflectionException + * @phpstan-return array|null */ public static function parseModelDoc(ReflectionClass $modelReflection): ?array { @@ -40,19 +41,21 @@ public static function parseModelDoc(ReflectionClass $modelReflection): ?array if ($m !== false && $m !== 0) { [, $returnString, $property] = $matches; $returnType = Type::fromString($returnString); + if (!$returnType->isBuiltin() && $returnType->isSimple()) { + if ($returnType->allows('null')) { + $returnType = Type::fromString( + /** @phpstan-ignore argument.type,argument.type */ + '?' . Reflection::expandClassName($returnType->getSingleName(), $modelReflection) + ); + } else { + $returnType = Type::fromString( + /** @phpstan-ignore argument.type,argument.type */ + Reflection::expandClassName($returnType->getSingleName(), $modelReflection) + ); + } + } $properties[$property] = [ 'type' => $returnType, - 'reflection' => $returnType->isClass() - ? new ReflectionClass( - /** @phpstan-ignore argument.type */ - Reflection::expandClassName( - /** @phpstan-ignore argument.type */ - $returnType->getSingleName(), - /** @phpstan-ignore argument.type */ - $modelReflection - ) - ) - : null, 'property' => $property, ]; } @@ -143,10 +146,8 @@ public static function resolveReferencedProperties(ReflectionClass $model): arra if (!$type->isClass()) { continue; } - $itemReflection = new ReflectionClass( - /** @phpstan-ignore argument.type,argument.type,argument.type */ - Reflection::expandClassName($type->getSingleName(), $model) - ); + /** @phpstan-ignore argument.type */ + $itemReflection = new ReflectionClass($type->getSingleName()); if ($itemReflection->isSubclassOf(Model::class)) { $items[$itemReflection->name] = [ 'type' => 'property', @@ -183,7 +184,6 @@ public static function getPath( self::resolveReferencedProperties($model), self::resolveReferencedMethods($model) ); - if (isset($items[$requestedModel->getName()])) { return [$items[$requestedModel->getName()]]; } diff --git a/src/Service/Service.php b/src/Service/Service.php index b778448..871bd78 100644 --- a/src/Service/Service.php +++ b/src/Service/Service.php @@ -8,29 +8,51 @@ use Fykosak\NetteORM\Model\Model; use Fykosak\NetteORM\Selection\TypedSelection; use Fykosak\NetteORM\Types\WGS84Point; +use Fykosak\NetteORM\Validator\Validator; +use Fykosak\NetteORM\Validator\ValidatorException; use Nette\Database\Explorer; use Nette\InvalidStateException; /** * @template TModel of Model + * @phpstan-type ColumnMeta array{ + * name: string, + * table: string, + * nativetype: string, + * size: ?int, + * nullable: bool, + * default: mixed, + * autoincrement: bool, + * primary: bool, + * comment: ?string, + * vendor: array + * } */ abstract class Service { - /** @phpstan-var array> */ + /** @phpstan-var list */ private array $columns; /** * @param Mapper $mapper + * @throws \ReflectionException */ final public function __construct( private readonly string $tableName, public readonly Explorer $explorer, private readonly Mapper $mapper ) { + if ($this->mapper->validate) { + $validator = new Validator(); + $errors = $validator($this); + if (count($errors) > 0) { + throw new ValidatorException($errors); + } + } } /** - * @phpstan-return TModel|null + * @phpstan-return Model|null */ public function findByPrimary(string|int|null $key): ?Model { @@ -38,7 +60,7 @@ public function findByPrimary(string|int|null $key): ?Model } /** - * @phpstan-param TModel $model + * @phpstan-param Model $model * @throws \PDOException */ public function disposeModel(Model $model): void @@ -63,9 +85,9 @@ final public function getTable(): TypedSelection } /** - * @phpstan-param TModel|null $model + * @phpstan-param Model|null $model * @phpstan-param array $data - * @phpstan-return TModel + * @phpstan-return Model * @throws \PDOException */ public function storeModel(array $data, ?Model $model = null): Model @@ -89,7 +111,7 @@ final public function getModelClassName(): string /** * @throws \InvalidArgumentException - * @phpstan-param TModel $model + * @phpstan-param Model $model */ protected function checkType(Model $model): void { @@ -124,9 +146,9 @@ protected function filterData(array $data): array } /** - * @phpstan-return array> + * @return list */ - protected function getColumnMetadata(): array + public function getColumnMetadata(): array { if (!isset($this->columns)) { $this->columns = $this->explorer->getConnection()->getDriver()->getColumns($this->tableName); diff --git a/src/Validator/CheckBuildIn.php b/src/Validator/CheckBuildIn.php new file mode 100644 index 0000000..60a0ab3 --- /dev/null +++ b/src/Validator/CheckBuildIn.php @@ -0,0 +1,33 @@ +isBuiltin() || $docType->isBuiltin()) { + if ($dbType->getSingleName() !== $docType->getSingleName()) { + $errors[] = new ValidatorError( + $table, + $column, + sprintf( + 'Database type `%s` does not match PHPDoc type `%s`', + $dbType, + $docType, + ) + ); + } + } + } +} diff --git a/src/Validator/CheckClass.php b/src/Validator/CheckClass.php new file mode 100644 index 0000000..512002d --- /dev/null +++ b/src/Validator/CheckClass.php @@ -0,0 +1,37 @@ +isBuiltin() && !$docType->isBuiltin()) { + if ( + $docType->getSingleName() !== $dbType->getSingleName() && + /** @phpstan-ignore argument.type,argument.type */ + !new \ReflectionClass($docType->getSingleName())->isSubclassOf($dbType->getSingleName()) + ) { + $errors[] = new ValidatorError( + $table, + $column, + sprintf( + 'Database type `%s` does not accept PHPDoc type `%s`', + $dbType, + $docType, + ) + ); + } + } + } +} diff --git a/src/Validator/CheckEnum.php b/src/Validator/CheckEnum.php new file mode 100644 index 0000000..f7d8f26 --- /dev/null +++ b/src/Validator/CheckEnum.php @@ -0,0 +1,61 @@ +getSingleName()); + } catch (\ReflectionException $exception) { + $errors[] = new ValidatorError( + $table, + $column, + $exception->getMessage() + ); + return; + } + + /** @var class-string<\BackedEnum> $enumClassName */ + $enumClassName = $docReflection->getName(); + $enumCases = $enumClassName::cases(); + /** @phpstan-ignore argument.type */ + foreach (explode(',', substr($metadata['vendor']['type'], 5, -1)) as $raw) { + $trimmedCase = substr($raw, 1, -1); + try { + $case = $enumClassName::from($trimmedCase); + $enumCases = array_filter($enumCases, fn($c) => $c === $case); + } catch (\ValueError $e) { + $errors[] = new ValidatorError( + $metadata['table'], + $metadata['name'], + $e->getMessage(), + ); + } + } + foreach ($enumCases as $missing) { + $errors[] = new ValidatorError( + $metadata['table'], + $metadata['name'], + sprintf( + 'Case `%s` is missing in DB.', + $missing->value, + ) + ); + } + } + } +} diff --git a/src/Validator/CheckNull.php b/src/Validator/CheckNull.php new file mode 100644 index 0000000..39b54ef --- /dev/null +++ b/src/Validator/CheckNull.php @@ -0,0 +1,31 @@ +allows('null') xor $docType->allows('null')) { + $errors[] = new ValidatorError( + $table, + $column, + sprintf( + 'Database %s NULL but PHPDoc %s NULL', + $dbType->allows('null') ? 'allows' : 'doesnt allow', + $docType->allows('null') ? 'allows' : 'doesnt allow', + ) + ); + } + } +} diff --git a/src/Validator/Validator.php b/src/Validator/Validator.php new file mode 100644 index 0000000..23068a5 --- /dev/null +++ b/src/Validator/Validator.php @@ -0,0 +1,143 @@ + $service + * @return ValidatorError[] + * @throws \ReflectionException + * @throws \Exception + */ + public function __invoke(Service $service): array + { + $doc = ModelRelationsParser::parseModelDoc(new \ReflectionClass($service->getModelClassName())); + $metaData = $service->getColumnMetadata(); + $exceptions = []; + foreach ($metaData as $metaDatum) { + $exceptions = [ + ...$exceptions, + ...$this->validateColumn($metaDatum, $doc[$metaDatum['name']] ?? null) + ]; + } + return $exceptions; + } + + /** + * @phpstan-param ColumnMeta $metadata + * @phpstan-param ColumnDoc|null $doc + * @return ValidatorError[] + * @throws \Exception + */ + private function validateColumn(array $metadata, array|null $doc): array + { + $errors = []; + $name = $metadata['name']; + if (!isset($doc)) { + $errors[] = new ValidatorError( + $metadata['table'], + $metadata['name'], + \sprintf( + 'Missing columns `%s`', + $name, + ) + ); + return $errors; + } + $dbType = $this->matchType($metadata, $errors); + if (isset($dbType)) { + new CheckNull()->validate($dbType, $doc['type'], $metadata, $metadata['table'], $metadata['name'], $errors); + new CheckBuildIn()->validate($dbType, $doc['type'], $metadata, $metadata['table'], $metadata['name'], $errors); + new CheckClass()->validate($dbType, $doc['type'], $metadata, $metadata['table'], $metadata['name'], $errors); + new CheckEnum()->validate($dbType, $doc['type'], $metadata, $metadata['table'], $metadata['name'], $errors); + } + return $errors; + } + + /** + * @phpstan-param ColumnMeta $metadata + * @phpstan-param ValidatorError[] $errors + */ + private function matchType(array $metadata, array &$errors): ?Type + { + if ($metadata['nativetype'] === 'BOOL') { + return $metadata['nullable'] ? Type::fromString('?bool') : Type::fromString('bool'); + } + + if ( + in_array( + $metadata['nativetype'], + ['DECIMAL', 'NUMERIC', 'FLOAT', 'DOUBLE', 'REAL', 'DOUBLE PRECISION'], + true + ) + ) { + return $metadata['nullable'] ? Type::fromString('?float') : Type::fromString('float'); + } + + if (in_array($metadata['nativetype'], ['VARCHAR', 'CHAR', 'TEXT', 'LONGTEXT'], true)) { + return $metadata['nullable'] ? Type::fromString('?string') : Type::fromString('string'); + } + if (in_array($metadata['nativetype'], ['TIMESTAMP', 'DATETIME', 'DATE'], true)) { + return $metadata['nullable'] ? + Type::fromString('?' . \DateTimeInterface::class) : + Type::fromString(\DateTimeInterface::class); + } + if ($metadata['nativetype'] === 'ENUM') { + return $metadata['nullable'] ? + Type::fromString('?' . \BackedEnum::class) : + Type::fromString(\BackedEnum::class); + } + if ($metadata['nativetype'] === 'TINYINT' && $metadata['size'] === 1) { + return $metadata['nullable'] ? Type::fromString('?bool') : Type::fromString('bool'); + } + if ($metadata['nativetype'] === 'POINT') { + return $metadata['nullable'] ? + Type::fromString('?' . WGS84Point::class) : + Type::fromString(WGS84Point::class); + } + if ( + in_array( + $metadata['nativetype'], + [ + 'TINYINT', + 'TINYINT UNSIGNED', + 'SMALLINT', + 'SMALLINT UNSIGNED', + 'MEDIUMINT', + 'MEDIUMINT UNSIGNED', + 'INT', + 'INT UNSIGNED', + 'BIGINT', + 'BIGINT UNSIGNED' + ], + true + ) + ) { + return $metadata['nullable'] ? Type::fromString('?int') : Type::fromString('int'); + } + + $errors[] = new ValidatorError( + $metadata['table'], + $metadata['name'], + sprintf( + 'Validator does not support type `%s`.', + $metadata['nativetype'], + ) + ); + return null; + } +} diff --git a/src/Validator/ValidatorCheck.php b/src/Validator/ValidatorCheck.php new file mode 100644 index 0000000..76f7612 --- /dev/null +++ b/src/Validator/ValidatorCheck.php @@ -0,0 +1,25 @@ +table, $this->column, $this->message); + } +} diff --git a/src/Validator/ValidatorException.php b/src/Validator/ValidatorException.php new file mode 100644 index 0000000..cce0eb9 --- /dev/null +++ b/src/Validator/ValidatorException.php @@ -0,0 +1,19 @@ +eventService = $eventService; + } + + public function injectParticipantService(ParticipantService $participantService): void + { + $this->participantService = $participantService; + } + public function testSuccess(): void { - /** @var ParticipantService $serviceParticipant */ - $serviceParticipant = $this->container->getByType(ParticipantService::class); /** @var ParticipantModel $participant */ - $participant = $serviceParticipant->getTable()->fetch(); + $participant = $this->participantService->getTable()->fetch(); $modelEvent = $participant->getReferencedModel(EventModel::class); Assert::type(EventModel::class, $modelEvent); } public function testSame(): void { - /** @var ParticipantService $serviceParticipant */ - $serviceParticipant = $this->container->getByType(ParticipantService::class); /** @var ParticipantModel $participant */ - $participant = $serviceParticipant->getTable()->fetch(); + $participant = $this->participantService->getTable()->fetch(); $newModel = $participant->getReferencedModel(ParticipantModel::class); Assert::same($participant, $newModel); } public function testNoCandidate(): void { - /** @var EventService $service */ - $service = $this->container->getByType(EventService::class); /** @var EventModel $event */ - $event = $service->getTable()->fetch(); + $event = $this->eventService->getTable()->fetch(); Assert::exception(function () use ($event) { $event->getReferencedModel(ParticipantModel::class); }, CannotAccessModelException::class); diff --git a/tests/Tests/ServiceOperationTest.phpt b/tests/Tests/ServiceOperationTest.phpt index 803168b..fb31124 100644 --- a/tests/Tests/ServiceOperationTest.phpt +++ b/tests/Tests/ServiceOperationTest.phpt @@ -14,15 +14,25 @@ require_once __DIR__ . '/TestCase.php'; class ServiceOperationTest extends TestCase { + private EventService $eventService; + private ParticipantService $participantService; + + public function injectEventService(EventService $eventService): void + { + $this->eventService = $eventService; + } + + public function injectParticipantService(ParticipantService $participantService): void + { + $this->participantService = $participantService; + } public function testCreateSuccess(): void { - /** @var ParticipantService $serviceParticipant */ - $serviceParticipant = $this->container->getByType(ParticipantService::class); - $countBefore = $serviceParticipant->getTable()->count('*'); + $countBefore = $this->participantService->getTable()->count('*'); - $newEvent = $serviceParticipant->storeModel(['event_id' => 1, 'name' => 'Igor']); - $countAfter = $serviceParticipant->getTable()->count('*'); + $newEvent = $this->participantService->storeModel(['event_id' => 1, 'name' => 'Igor']); + $countAfter = $this->participantService->getTable()->count('*'); Assert::same($countBefore + 1, $countAfter); Assert::type(ParticipantModel::class, $newEvent); @@ -31,68 +41,56 @@ class ServiceOperationTest extends TestCase public function testCreateError(): void { - /** @var ParticipantService $serviceParticipant */ - $serviceParticipant = $this->container->getByType(ParticipantService::class); - $countBefore = $serviceParticipant->getTable()->count('*'); - Assert::exception(function () use ($serviceParticipant) { - $serviceParticipant->storeModel(['event_id' => 4, 'name' => 'Igor']); + $countBefore = $this->participantService->getTable()->count('*'); + Assert::exception(function () { + $this->participantService->storeModel(['event_id' => 4, 'name' => 'Igor']); }, ForeignKeyConstraintViolationException::class); - $countAfter = $serviceParticipant->getTable()->count('*'); + $countAfter = $this->participantService->getTable()->count('*'); Assert::same($countBefore, $countAfter); } public function testFindByPrimary(): void { - /** @var ParticipantService $serviceParticipant */ - $serviceParticipant = $this->container->getByType(ParticipantService::class); - $participant = $serviceParticipant->findByPrimary(1); + $participant = $this->participantService->findByPrimary(1); Assert::same('Adam', $participant->name); Assert::type(ParticipantModel::class, $participant); - $nullParticipant = $serviceParticipant->findByPrimary(10); + $nullParticipant = $this->participantService->findByPrimary(10); Assert::null($nullParticipant); } public function testFindByPrimaryProtection(): void { - /** @var ParticipantService $serviceParticipant */ - $serviceParticipant = $this->container->getByType(ParticipantService::class); - $participant = $serviceParticipant->findByPrimary(null); + $participant = $this->participantService->findByPrimary(null); Assert::null($participant); } public function testUpdateSuccess(): void { - /** @var ParticipantService $serviceParticipant */ - $serviceParticipant = $this->container->getByType(ParticipantService::class); - $participant = $serviceParticipant->findByPrimary(2); - $serviceParticipant->storeModel(['name' => 'Betka'], $participant); + $participant = $this->participantService->findByPrimary(2); + $this->participantService->storeModel(['name' => 'Betka'], $participant); Assert::same('Betka', $participant->name); Assert::type(ParticipantModel::class, $participant); } public function testUpdateError(): void { - /** @var ParticipantService $serviceParticipant */ - $serviceParticipant = $this->container->getByType(ParticipantService::class); - $participant = $serviceParticipant->findByPrimary(2); + $participant = $this->participantService->findByPrimary(2); - Assert::exception(function () use ($participant, $serviceParticipant) { - $serviceParticipant->storeModel(['event_id' => 4], $participant); + Assert::exception(function () use ($participant) { + $this->participantService->storeModel(['event_id' => 4], $participant); }, ForeignKeyConstraintViolationException::class); Assert::same('Bára', $participant->name); } public function testStoreExists(): void { - /** @var ParticipantService $serviceParticipant */ - $serviceParticipant = $this->container->getByType(ParticipantService::class); - $participant = $serviceParticipant->findByPrimary(2); - $newParticipant = $serviceParticipant->storeModel(['name' => 'Betka'], $participant); + $participant = $this->participantService->findByPrimary(2); + $newParticipant = $this->participantService->storeModel(['name' => 'Betka'], $participant); Assert::same($participant, $newParticipant);// must be a same obj Assert::same('Betka', $participant->name); Assert::type(ParticipantModel::class, $participant); @@ -100,40 +98,33 @@ class ServiceOperationTest extends TestCase public function testStoreNew(): void { - /** @var ParticipantService $serviceParticipant */ - $serviceParticipant = $this->container->getByType(ParticipantService::class); - $newParticipant = $serviceParticipant->storeModel(['event_id' => 1, 'name' => 'Igor'], null); + $newParticipant = $this->participantService->storeModel(['event_id' => 1, 'name' => 'Igor'], null); Assert::same('Igor', $newParticipant->name); Assert::type(ParticipantModel::class, $newParticipant); } public function testDelete(): void { - /** @var ParticipantService $serviceParticipant */ - $serviceParticipant = $this->container->getByType(ParticipantService::class); - $participant = $serviceParticipant->findByPrimary(2); - $countBefore = $serviceParticipant->getTable()->count('*'); - $serviceParticipant->disposeModel($participant); - $countAfter = $serviceParticipant->getTable()->count('*'); + $participant = $this->participantService->findByPrimary(2); + $countBefore = $this->participantService->getTable()->count('*'); + $this->participantService->disposeModel($participant); + $countAfter = $this->participantService->getTable()->count('*'); Assert::same($countBefore - 1, $countAfter); } public function testType(): void { - $serviceParticipant = $this->container->getByType(ParticipantService::class); - $serviceEvent = $this->container->getByType(EventService::class); - $event = $serviceEvent->getTable()->fetch(); - Assert::exception(function () use ($event, $serviceParticipant) { - $serviceParticipant->disposeModel($event); + $event = $this->eventService->getTable()->fetch(); + Assert::exception(function () use ($event) { + $this->participantService->disposeModel($event); }, \TypeError::class); } public function testDeleteError(): void { - $serviceEvent = $this->container->getByType(EventService::class); - $event = $serviceEvent->getTable()->fetch(); - Assert::exception(function () use ($event, $serviceEvent) { - $serviceEvent->disposeModel($event); + $event = $this->eventService->getTable()->fetch(); + Assert::exception(function () use ($event) { + $this->eventService->disposeModel($event); }, ForeignKeyConstraintViolationException::class); } } diff --git a/tests/Tests/TableSelectionTest.phpt b/tests/Tests/TableSelectionTest.phpt index 351e1ed..6a478e7 100644 --- a/tests/Tests/TableSelectionTest.phpt +++ b/tests/Tests/TableSelectionTest.phpt @@ -4,11 +4,11 @@ declare(strict_types=1); namespace Fykosak\NetteORM\Tests\Tests; +use Fykosak\NetteORM\Selection\TypedSelection; use Fykosak\NetteORM\Tests\ORM\EventModel; -use Fykosak\NetteORM\Tests\ORM\ParticipantModel; use Fykosak\NetteORM\Tests\ORM\EventService; +use Fykosak\NetteORM\Tests\ORM\ParticipantModel; use Fykosak\NetteORM\Tests\ORM\ParticipantService; -use Fykosak\NetteORM\Selection\TypedSelection; use Nette\Database\Table\ActiveRow; use Nette\Database\Table\Selection; use Tester\Assert; @@ -17,12 +17,22 @@ require_once __DIR__ . '/TestCase.php'; class TableSelectionTest extends TestCase { + private EventService $eventService; + private ParticipantService $participantService; + + public function injectEventService(EventService $eventService): void + { + $this->eventService = $eventService; + } + + public function injectParticipantService(ParticipantService $participantService): void + { + $this->participantService = $participantService; + } public function testType(): void { - /** @var EventService $serviceEvent */ - $serviceEvent = $this->container->getByType(EventService::class); - $selection = $serviceEvent->getTable(); + $selection = $this->eventService->getTable(); Assert::type(TypedSelection::class, $selection); Assert::type(Selection::class, $selection); $event = $selection->fetch(); @@ -32,9 +42,7 @@ class TableSelectionTest extends TestCase public function testReferenced(): void { - /** @var ParticipantService $serviceEvent */ - $serviceEvent = $this->container->getByType(ParticipantService::class); - $participant = $serviceEvent->getTable()->fetch(); + $participant = $this->participantService->getTable()->fetch(); Assert::type(ParticipantModel::class, $participant); $event = $participant->event; @@ -43,9 +51,7 @@ class TableSelectionTest extends TestCase public function testRelated(): void { - /** @var ParticipantService $serviceEvent */ - $serviceEvent = $this->container->getByType(EventService::class); - $event = $serviceEvent->getTable()->fetch(); + $event = $this->eventService->getTable()->fetch(); $participant = $event->related('participant')->fetch(); Assert::type(ParticipantModel::class, $participant); } diff --git a/tests/Tests/TestCase.php b/tests/Tests/TestCase.php index 64ee3c6..fe0042e 100644 --- a/tests/Tests/TestCase.php +++ b/tests/Tests/TestCase.php @@ -36,6 +36,7 @@ public function __construct() }); $this->container = new $class(); + $this->container->callInjects($this); } protected function setUp(): void diff --git a/tests/config.neon b/tests/config.neon index 66dafa8..05f1210 100644 --- a/tests/config.neon +++ b/tests/config.neon @@ -1,12 +1,14 @@ services: - Nette\Caching\Storages\FileStorage(/tmp) orm: - event: - model: Fykosak\NetteORM\Tests\ORM\EventModel - service: Fykosak\NetteORM\Tests\ORM\EventService - participant: - model: Fykosak\NetteORM\Tests\ORM\ParticipantModel - service: Fykosak\NetteORM\Tests\ORM\ParticipantService + validation: true + tables: + event: + model: Fykosak\NetteORM\Tests\ORM\EventModel + service: Fykosak\NetteORM\Tests\ORM\EventService + participant: + model: Fykosak\NetteORM\Tests\ORM\ParticipantModel + service: Fykosak\NetteORM\Tests\ORM\ParticipantService includes: - config.local.neon diff --git a/tests/resource/schema.sql b/tests/resource/schema.sql index efa122c..7895c13 100644 --- a/tests/resource/schema.sql +++ b/tests/resource/schema.sql @@ -16,7 +16,7 @@ CREATE TABLE `participant` `participant_id` INT PRIMARY KEY AUTO_INCREMENT NOT NULL, `event_id` INT NOT NULL, `status` ENUM ('applied','cancelled') NOT NULL DEFAULT 'applied', - `name` VARCHAR(64), + `name` VARCHAR(64) NOT NULL, CONSTRAINT `fk_participant_event` FOREIGN KEY (`event_id`) REFERENCES `event` (`event_id`)