diff --git a/src/generator/default/dbmodel.php b/src/generator/default/dbmodel.php index d9c60ebc..bf04a385 100644 --- a/src/generator/default/dbmodel.php +++ b/src/generator/default/dbmodel.php @@ -87,7 +87,7 @@ public function rules() public function getgetCamelName() ?>() { return $this->getMethod() ?>(\\getClassName() ?>::class, linkToString()?>); + echo $relation->linkToString()?>)getInverse() ? '->inverseOf(\''.$relation->getInverse().'\')' : '' ?>; } many2many as $relation): ?> @@ -103,4 +103,12 @@ public function getgetCamelName() ?>() } +inverseRelations as $relationName => $relation): ?> + + public function getgetCamelName().($i===1 ? '' : $i) ?>() + { + return $this->getMethod() ?>(\\getClassName() ?>::class, linkToString() ?>)->inverseOf('getInverse() ?>'); + } + } diff --git a/src/lib/AttributeResolver.php b/src/lib/AttributeResolver.php index 0063d8df..d25a28c2 100644 --- a/src/lib/AttributeResolver.php +++ b/src/lib/AttributeResolver.php @@ -7,8 +7,6 @@ namespace cebe\yii2openapi\lib; -use cebe\yii2openapi\lib\Config; -use cebe\yii2openapi\lib\CustomSpecAttr; use cebe\yii2openapi\lib\exceptions\InvalidDefinitionException; use cebe\yii2openapi\lib\items\Attribute; use cebe\yii2openapi\lib\items\AttributeRelation; @@ -20,9 +18,9 @@ use cebe\yii2openapi\lib\openapi\ComponentSchema; use cebe\yii2openapi\lib\openapi\PropertySchema; use Yii; +use yii\base\InvalidConfigException; use yii\helpers\Inflector; use yii\helpers\StringHelper; -use yii\helpers\VarDumper; use function explode; use function strpos; use function strtolower; @@ -32,49 +30,40 @@ class AttributeResolver /** * @var Attribute[]|array */ - private $attributes = []; + private array $attributes = []; /** * @var AttributeRelation[]|array */ - private $relations = []; + public array $relations = []; + /** * @var NonDbRelation[]|array */ - private $nonDbRelations = []; + private array $nonDbRelations = []; /** * @var ManyToManyRelation[]|array */ - private $many2many = []; + private array $many2many = []; - /** - * @var string - */ - private $schemaName; + private string $schemaName; - /** - * @var string - */ - private $tableName; + private string $tableName; - /** - * @var ComponentSchema - */ - private $schema; + private ComponentSchema $schema; - /** - * @var \cebe\yii2openapi\lib\items\JunctionSchemas - */ - private $junctions; + private JunctionSchemas $junctions; - /** @var bool */ - private $isJunctionSchema; + private bool $isJunctionSchema; - /** @var bool */ - private $hasMany2Many; + private bool $hasMany2Many; - /** @var Config */ - private $config; + private ?Config $config; + + /** + * @var AttributeRelation[]|array + */ + public array $inverseRelations = []; public function __construct(string $schemaName, ComponentSchema $schema, JunctionSchemas $junctions, ?Config $config = null) { @@ -88,14 +77,14 @@ public function __construct(string $schemaName, ComponentSchema $schema, Junctio } /** - * @return \cebe\yii2openapi\lib\items\DbModel - * @throws \cebe\yii2openapi\lib\exceptions\InvalidDefinitionException - * @throws \yii\base\InvalidConfigException + * @return DbModel + * @throws InvalidDefinitionException + * @throws InvalidConfigException */ - public function resolve():DbModel + public function resolve(): DbModel { foreach ($this->schema->getProperties() as $property) { - /** @var $property \cebe\yii2openapi\lib\openapi\PropertySchema */ + /** @var $property PropertySchema */ $isRequired = $this->schema->isRequiredProperty($property->getName()); $nullableValue = $property->getProperty()->getSerializableData()->nullable ?? null; @@ -111,6 +100,7 @@ public function resolve():DbModel $this->resolveProperty($property, $isRequired, $nullableValue); } } + return Yii::createObject(DbModel::class, [ [ 'pkName' => $this->schema->getPkName(), @@ -130,24 +120,24 @@ public function resolve():DbModel } /** - * @param \cebe\yii2openapi\lib\openapi\PropertySchema $property - * @param bool $isRequired - * @throws \cebe\yii2openapi\lib\exceptions\InvalidDefinitionException - * @throws \yii\base\InvalidConfigException + * @param PropertySchema $property + * @param bool $isRequired + * @throws InvalidDefinitionException + * @throws InvalidConfigException */ - protected function resolveJunctionTableProperty(PropertySchema $property, bool $isRequired):void + protected function resolveJunctionTableProperty(PropertySchema $property, bool $isRequired): void { if ($this->junctions->isJunctionProperty($this->schemaName, $property->getName())) { $junkAttribute = $this->junctions->byJunctionSchema($this->schemaName)[$property->getName()]; $attribute = Yii::createObject(Attribute::class, [$property->getName()]); $attribute->setRequired($isRequired) - ->setDescription($property->getAttr('description', '')) - ->setReadOnly($property->isReadonly()) - ->setIsPrimary($property->isPrimaryKey()) - ->asReference($junkAttribute['relatedClassName']) - ->setPhpType($junkAttribute['phpType']) - ->setDbType($junkAttribute['dbType']) - ->setForeignKeyColumnName($property->fkColName); + ->setDescription($property->getAttr('description', '')) + ->setReadOnly($property->isReadonly()) + ->setIsPrimary($property->isPrimaryKey()) + ->asReference($junkAttribute['relatedClassName']) + ->setPhpType($junkAttribute['phpType']) + ->setDbType($junkAttribute['dbType']) + ->setForeignKeyColumnName($property->fkColName); $relation = Yii::createObject(AttributeRelation::class, [ $property->getName(), $junkAttribute['relatedTableName'], @@ -162,12 +152,12 @@ protected function resolveJunctionTableProperty(PropertySchema $property, bool $ } /** - * @param \cebe\yii2openapi\lib\openapi\PropertySchema $property - * @param bool $isRequired - * @throws \cebe\yii2openapi\lib\exceptions\InvalidDefinitionException - * @throws \yii\base\InvalidConfigException + * @param PropertySchema $property + * @param bool $isRequired + * @throws InvalidDefinitionException + * @throws InvalidConfigException */ - protected function resolveHasMany2ManyTableProperty(PropertySchema $property, bool $isRequired):void + protected function resolveHasMany2ManyTableProperty(PropertySchema $property, bool $isRequired): void { if ($this->junctions->isManyToManyProperty($this->schemaName, $property->getName())) { return; @@ -197,7 +187,7 @@ protected function resolveHasMany2ManyTableProperty(PropertySchema $property, bo $this->relations[Inflector::pluralize($junkRef)] = Yii::createObject(AttributeRelation::class, [$junkRef, $junkAttribute['junctionTable'], $viaModel]) - ->asHasMany([$junkAttribute['pairProperty'] . '_id' => $this->schema->getPkName()]); + ->asHasMany([$junkAttribute['pairProperty'] . '_id' => $this->schema->getPkName()]); return; } @@ -205,35 +195,35 @@ protected function resolveHasMany2ManyTableProperty(PropertySchema $property, bo } /** - * @param \cebe\yii2openapi\lib\openapi\PropertySchema $property - * @param bool $isRequired - * @param bool|null|string $nullableValue if string then its value will be only constant `ARG_ABSENT`. Default `null` is avoided because it can be in passed value in method call - * @throws \cebe\yii2openapi\lib\exceptions\InvalidDefinitionException - * @throws \yii\base\InvalidConfigException + * @param PropertySchema $property + * @param bool $isRequired + * @param bool|null|string $nullableValue if string then its value will be only constant `ARG_ABSENT`. Default `null` is avoided because it can be in passed value in method call + * @throws InvalidDefinitionException + * @throws InvalidConfigException */ protected function resolveProperty( PropertySchema $property, bool $isRequired, $nullableValue = 'ARG_ABSENT' - ):void { + ): void { if ($nullableValue === 'ARG_ABSENT') { $nullableValue = $property->getProperty()->getSerializableData()->nullable ?? null; } $attribute = Yii::createObject(Attribute::class, [$property->getName()]); $attribute->setRequired($isRequired) - ->setDescription($property->getAttr('description', '')) - ->setReadOnly($property->isReadonly()) - ->setDefault($property->guessDefault()) - ->setXDbType($property->getAttr(CustomSpecAttr::DB_TYPE)) - ->setXDbDefaultExpression($property->getAttr(CustomSpecAttr::DB_DEFAULT_EXPRESSION)) - ->setNullable($nullableValue) - ->setIsPrimary($property->isPrimaryKey()) - ->setForeignKeyColumnName($property->fkColName); + ->setDescription($property->getAttr('description', '')) + ->setReadOnly($property->isReadonly()) + ->setDefault($property->guessDefault()) + ->setXDbType($property->getAttr(CustomSpecAttr::DB_TYPE)) + ->setXDbDefaultExpression($property->getAttr(CustomSpecAttr::DB_DEFAULT_EXPRESSION)) + ->setNullable($nullableValue) + ->setIsPrimary($property->isPrimaryKey()) + ->setForeignKeyColumnName($property->fkColName); if ($property->isReference()) { if ($property->isVirtual()) { throw new InvalidDefinitionException('References not supported for virtual attributes'); } - + if ($property->isNonDbReference()) { $attribute->asNonDbReference($property->getRefClassName()); $relation = Yii::createObject( @@ -258,31 +248,34 @@ protected function resolveProperty( [$min, $max] = $fkProperty->guessMinMax(); $attribute->asReference($relatedClassName); $attribute->setPhpType($fkProperty->guessPhpType()) - ->setDbType($fkProperty->guessDbType(true)) - ->setSize($fkProperty->getMaxLength()) - ->setDescription($property->getRefSchema()->getDescription()) - ->setDefault($fkProperty->guessDefault()) - ->setLimits($min, $max, $fkProperty->getMinLength()); + ->setDbType($fkProperty->guessDbType(true)) + ->setSize($fkProperty->getMaxLength()) + ->setDescription($property->getRefSchema()->getDescription()) + ->setDefault($fkProperty->guessDefault()) + ->setLimits($min, $max, $fkProperty->getMinLength()); $relation = Yii::createObject( AttributeRelation::class, [$property->getName(), $relatedTableName, $relatedClassName] ) - ->asHasOne([$fkProperty->getName() => $attribute->columnName]); + ->asHasOne([$fkProperty->getName() => $attribute->columnName]); $relation->onUpdateFkConstraint = $property->onUpdateFkConstraint; $relation->onDeleteFkConstraint = $property->onDeleteFkConstraint; if ($property->isRefPointerToSelf()) { $relation->asSelfReference(); } $this->relations[$property->getName()] = $relation; + if (!$property->isRefPointerToSelf()) { + $this->addInverseRelation($relatedClassName, $attribute, $property, $fkProperty); + } } if (!$property->isReference() && !$property->hasRefItems()) { [$min, $max] = $property->guessMinMax(); $attribute->setIsVirtual($property->isVirtual()) - ->setPhpType($property->guessPhpType()) - ->setDbType($property->guessDbType()) - ->setSize($property->getMaxLength()) - ->setLimits($min, $max, $property->getMinLength()); + ->setPhpType($property->guessPhpType()) + ->setDbType($property->guessDbType()) + ->setSize($property->getMaxLength()) + ->setLimits($min, $max, $property->getMinLength()); if ($property->hasEnum()) { $attribute->setEnumValues($property->getAttr('enum')); } @@ -319,7 +312,7 @@ protected function resolveProperty( AttributeRelation::class, [$property->getName(), $relatedTableName, $relatedClassName] ) - ->asHasMany([$fkProperty->getName() => $fkProperty->getName()])->asSelfReference(); + ->asHasMany([$fkProperty->getName() => $fkProperty->getName()])->asSelfReference(); return; } $foreignPk = Inflector::camel2id($fkProperty->getName(), '_') . '_id'; @@ -328,7 +321,7 @@ protected function resolveProperty( AttributeRelation::class, [$property->getName(), $relatedTableName, $relatedClassName] ) - ->asHasMany([$foreignPk => $this->schema->getPkName()]); + ->asHasMany([$foreignPk => $this->schema->getPkName()]); return; } $relatedClassName = $property->getRefClassName(); @@ -347,7 +340,8 @@ protected function resolveProperty( AttributeRelation::class, [$property->getName(), $relatedTableName, $relatedClassName] ) - ->asHasMany([Inflector::camel2id($this->schemaName, '_') . '_id' => $this->schema->getPkName()]); + ->asHasMany([Inflector::camel2id($this->schemaName, '_') . '_id' => $this->schema->getPkName()]) + ->setInverse(Inflector::variablize($this->schemaName)); return; } if ($this->schema->isNonDb() && $attribute->isReference()) { @@ -367,14 +361,14 @@ protected function resolveProperty( * @param string $relatedTableName * @param ComponentSchema $refSchema * @return bool - * @throws \yii\base\InvalidConfigException + * @throws InvalidConfigException|InvalidDefinitionException */ protected function catchManyToMany( string $propertyName, string $relatedSchemaName, string $relatedTableName, ComponentSchema $refSchema - ):bool { + ): bool { if (strtolower(Inflector::id2camel($propertyName, '_')) !== strtolower(Inflector::pluralize($relatedSchemaName))) { return false; @@ -406,9 +400,9 @@ protected function catchManyToMany( } /** - * @throws \yii\base\InvalidConfigException + * @throws InvalidConfigException */ - protected function guessFakerStub(Attribute $attribute, PropertySchema $property):?string + protected function guessFakerStub(Attribute $attribute, PropertySchema $property): ?string { $resolver = Yii::createObject(['class' => FakerStubResolver::class], [$attribute, $property, $this->config]); return $resolver->resolve(); @@ -417,9 +411,9 @@ protected function guessFakerStub(Attribute $attribute, PropertySchema $property /** * @param array $indexes * @return array|DbIndex[] - * @throws \cebe\yii2openapi\lib\exceptions\InvalidDefinitionException + * @throws InvalidDefinitionException */ - protected function prepareIndexes(array $indexes):array + protected function prepareIndexes(array $indexes): array { $dbIndexes = []; foreach ($indexes as $index) { @@ -470,12 +464,12 @@ protected function prepareIndexes(array $indexes):array } /** - * @param \cebe\yii2openapi\lib\openapi\PropertySchema $property - * @param \cebe\yii2openapi\lib\items\Attribute $attribute + * @param PropertySchema $property + * @param Attribute $attribute * @return void - * @throws \yii\base\InvalidConfigException + * @throws InvalidConfigException|InvalidDefinitionException */ - protected function resolvePropertyRef(PropertySchema $property, Attribute $attribute):void + protected function resolvePropertyRef(PropertySchema $property, Attribute $attribute): void { $fkProperty = new PropertySchema( $property->getRefSchema()->getSchema(), @@ -484,12 +478,30 @@ protected function resolvePropertyRef(PropertySchema $property, Attribute $attri ); [$min, $max] = $fkProperty->guessMinMax(); $attribute->setPhpType($fkProperty->guessPhpType()) - ->setDbType($fkProperty->guessDbType(true)) - ->setSize($fkProperty->getMaxLength()) - ->setDescription($fkProperty->getAttr('description')) - ->setDefault($fkProperty->guessDefault()) - ->setLimits($min, $max, $fkProperty->getMinLength()); + ->setDbType($fkProperty->guessDbType(true)) + ->setSize($fkProperty->getMaxLength()) + ->setDescription($fkProperty->getAttr('description')) + ->setDefault($fkProperty->guessDefault()) + ->setLimits($min, $max, $fkProperty->getMinLength()); $this->attributes[$property->getName()] = $attribute->setFakerStub($this->guessFakerStub($attribute, $fkProperty)); } + + /** + * @throws InvalidConfigException + */ + public function addInverseRelation( + string $relatedClassName, + Attribute $attribute, + PropertySchema $property, + PropertySchema $fkProperty + ): void { + $inverseRelation = Yii::createObject( + AttributeRelation::class, + [$this->schemaName, $this->tableName, $this->schemaName] + ) + ->asHasOne([$attribute->columnName => $fkProperty->getName()]); + $inverseRelation->setInverse($property->getName()); + $this->inverseRelations[$relatedClassName][] = $inverseRelation; + } } diff --git a/src/lib/SchemaToDatabase.php b/src/lib/SchemaToDatabase.php index 0e93ff29..af2521b2 100644 --- a/src/lib/SchemaToDatabase.php +++ b/src/lib/SchemaToDatabase.php @@ -7,10 +7,17 @@ namespace cebe\yii2openapi\lib; +use cebe\openapi\exceptions\IOException; +use cebe\openapi\exceptions\TypeErrorException; +use cebe\openapi\exceptions\UnresolvableReferenceException; +use cebe\yii2openapi\lib\exceptions\InvalidDefinitionException; +use cebe\yii2openapi\lib\items\AttributeRelation; +use cebe\yii2openapi\lib\items\DbModel; use cebe\yii2openapi\lib\items\JunctionSchemas; use cebe\yii2openapi\lib\openapi\ComponentSchema; use Yii; use yii\base\Exception; +use yii\base\InvalidConfigException; use yii\helpers\StringHelper; use function count; @@ -55,10 +62,7 @@ */ class SchemaToDatabase { - /** - * @var \cebe\yii2openapi\lib\Config - */ - protected $config; + protected Config $config; public function __construct(Config $config) { @@ -66,17 +70,22 @@ public function __construct(Config $config) } /** - * @return array|\cebe\yii2openapi\lib\items\DbModel[] - * @throws \cebe\openapi\exceptions\IOException - * @throws \cebe\openapi\exceptions\TypeErrorException - * @throws \cebe\openapi\exceptions\UnresolvableReferenceException - * @throws \cebe\yii2openapi\lib\exceptions\InvalidDefinitionException - * @throws \yii\base\Exception - * @throws \yii\base\InvalidConfigException + * @return array|DbModel[] + * @throws IOException + * @throws TypeErrorException + * @throws UnresolvableReferenceException + * @throws InvalidDefinitionException + * @throws Exception + * @throws InvalidConfigException */ - public function prepareModels():array + public function prepareModels(): array { + /** @var DbModel[] $models */ $models = []; + + /** @var AttributeResolver[] $resolvers */ + $resolvers = []; + $openApi = $this->config->getOpenApi(); $junctions = $this->findJunctionSchemas(); foreach ($openApi->components->schemas as $schemaName => $openApiSchema) { @@ -88,11 +97,25 @@ public function prepareModels():array if ($junctions->isJunctionSchema($schemaName)) { $schemaName = $junctions->trimPrefix($schemaName); } - /**@var \cebe\yii2openapi\lib\AttributeResolver $resolver */ + /**@var AttributeResolver $resolver */ $resolver = Yii::createObject(AttributeResolver::class, [$schemaName, $schema, $junctions, $this->config]); - $models[$schemaName] = $resolver->resolve(); + + // $models[$schemaName] = $resolver->resolve(); + $resolvers[$schemaName] = $resolver; + $models[$schemaName] = $resolvers[$schemaName]->resolve(); } - foreach ($models as $model) { + + // handle inverse relation + foreach ($resolvers as $aResolver) { + foreach ($aResolver->inverseRelations as $name => $relations) { + foreach ($relations as $relation) { + /** @var AttributeRelation $relation */ + $models[$name]->inverseRelations[] = $relation; + } + } + } + + foreach ($models as $model) { foreach ($model->many2many as $relation) { if (isset($models[$relation->viaModelName])) { $relation->hasViaModel = true; @@ -102,20 +125,18 @@ public function prepareModels():array } } - // TODO generate inverse relations - return $models; } /** - * @return \cebe\yii2openapi\lib\items\JunctionSchemas - * @throws \cebe\openapi\exceptions\IOException - * @throws \cebe\openapi\exceptions\TypeErrorException - * @throws \cebe\openapi\exceptions\UnresolvableReferenceException - * @throws \yii\base\Exception - * @throws \yii\base\InvalidConfigException + * @return JunctionSchemas + * @throws IOException + * @throws TypeErrorException + * @throws UnresolvableReferenceException + * @throws Exception + * @throws InvalidConfigException|InvalidDefinitionException */ - public function findJunctionSchemas():JunctionSchemas + public function findJunctionSchemas(): JunctionSchemas { $junctions = []; $openApi = $this->config->getOpenApi(); @@ -195,7 +216,7 @@ public function findJunctionSchemas():JunctionSchemas return Yii::createObject(JunctionSchemas::class, [$junctions]); } - private function canGenerateModel(string $schemaName, ComponentSchema $schema):bool + private function canGenerateModel(string $schemaName, ComponentSchema $schema): bool { // only generate tables for schemas of type object and those who have defined properties if ($schema->isObjectSchema() && !$schema->hasProperties()) { diff --git a/src/lib/items/AttributeRelation.php b/src/lib/items/AttributeRelation.php index 7b21fa76..31acbd2c 100644 --- a/src/lib/items/AttributeRelation.php +++ b/src/lib/items/AttributeRelation.php @@ -7,10 +7,10 @@ namespace cebe\yii2openapi\lib\items; +use cebe\yii2openapi\lib\traits\ForeignKeyConstraints; use yii\helpers\Inflector; use yii\helpers\VarDumper; use function reset; -use cebe\yii2openapi\lib\traits\ForeignKeyConstraints; class AttributeRelation { @@ -19,40 +19,29 @@ class AttributeRelation public const HAS_ONE = 'hasOne'; public const HAS_MANY = 'hasMany'; - /** - * @var string $name - **/ - private $name; + private string $name; - /** - * @var string $tableName - **/ - private $tableName; + private ?string $tableName; - /** - * @var string $className - **/ - private $className; + private ?string $className; /** - * @var string $method (hasOne/hasMany) - **/ - private $method; + * hasOne/hasMany + */ + private ?string $method; - /** - * @var array - **/ - private $link = []; + private array $link; + + private bool $selfReference = false; - /**@var bool */ - private $selfReference = false; + private ?string $inverse = null; public function __construct( - string $name, + string $name, ?string $tableName = null, ?string $className = null, ?string $method = null, - array $link = [] + array $link = [] ) { $this->name = $name; $this->tableName = $tableName; @@ -65,7 +54,7 @@ public function __construct( * @param string $name * @return AttributeRelation */ - public function setName(string $name):AttributeRelation + public function setName(string $name): AttributeRelation { $this->name = $name; return $this; @@ -75,7 +64,7 @@ public function setName(string $name):AttributeRelation * @param string $tableName * @return AttributeRelation */ - public function setTableName(string $tableName):AttributeRelation + public function setTableName(string $tableName): AttributeRelation { $this->tableName = $tableName; return $this; @@ -85,38 +74,38 @@ public function setTableName(string $tableName):AttributeRelation * @param string $className * @return AttributeRelation */ - public function setClassName(string $className):AttributeRelation + public function setClassName(string $className): AttributeRelation { $this->className = $className; return $this; } - public function asSelfReference():AttributeRelation + public function asSelfReference(): AttributeRelation { $this->selfReference = true; return $this; } - public function asHasOne(array $link):AttributeRelation + public function asHasOne(array $link): AttributeRelation { $this->method = self::HAS_ONE; $this->link = $link; return $this; } - public function asHasMany(array $link):AttributeRelation + public function asHasMany(array $link): AttributeRelation { $this->method = self::HAS_MANY; $this->link = $link; return $this; } - public function isHasOne():bool + public function isHasOne(): bool { return $this->method === self::HAS_ONE; } - public function isSelfReferenced():bool + public function isSelfReferenced(): bool { return $this->selfReference; } @@ -124,7 +113,7 @@ public function isSelfReferenced():bool /** * @return string */ - public function getName():string + public function getName(): string { return $this->name; } @@ -132,12 +121,12 @@ public function getName():string /** * @return string */ - public function getTableName():string + public function getTableName(): string { return $this->tableName; } - public function getTableAlias():string + public function getTableAlias(): string { return "{{%$this->tableName}}"; } @@ -145,12 +134,12 @@ public function getTableAlias():string /** * @return string */ - public function getClassName():string + public function getClassName(): string { return $this->className; } - public function getClassKey():string + public function getClassKey(): string { return Inflector::camel2id($this->getClassName()); } @@ -158,7 +147,7 @@ public function getClassKey():string /** * @return string */ - public function getMethod():string + public function getMethod(): string { return $this->method; } @@ -166,27 +155,27 @@ public function getMethod():string /** * @return array */ - public function getLink():array + public function getLink(): array { return $this->link; } - public function getCamelName():string + public function getCamelName(): string { return Inflector::camelize($this->name); } - public function getColumnName():string + public function getColumnName(): string { return reset($this->link); } - public function getForeignName():string + public function getForeignName(): string { return key($this->link); } - public function linkToString():string + public function linkToString(): string { return str_replace( [',', '=>', ', ]'], @@ -194,4 +183,15 @@ public function linkToString():string preg_replace('~\s+~', '', VarDumper::export($this->getLink())) ); } + + public function setInverse(string $inverse): AttributeRelation + { + $this->inverse = $inverse; + return $this; + } + + public function getInverse(): ?string + { + return $this->inverse; + } } diff --git a/src/lib/items/DbModel.php b/src/lib/items/DbModel.php index 4f174370..5f3c8c4e 100644 --- a/src/lib/items/DbModel.php +++ b/src/lib/items/DbModel.php @@ -10,9 +10,9 @@ use cebe\yii2openapi\lib\ValidationRulesBuilder; use Yii; use yii\base\BaseObject; +use yii\base\InvalidConfigException; use yii\db\ColumnSchema; use yii\helpers\Inflector; -use yii\helpers\StringHelper; use yii\helpers\VarDumper; use function array_filter; use function array_map; @@ -20,10 +20,10 @@ use const PHP_EOL; /** - * @property-read string $tableAlias - * @property-read array $uniqueColumnsList - * @property-read array[]|array $attributesByType - * @property-read array|\cebe\yii2openapi\lib\items\AttributeRelation[] $hasOneRelations + * @property-read string $tableAlias + * @property-read array $uniqueColumnsList + * @property-read array[]|array $attributesByType + * @property-read array|AttributeRelation[] $hasOneRelations */ class DbModel extends BaseObject { @@ -32,61 +32,63 @@ class DbModel extends BaseObject */ public $pkName; - /** - * @var string model name. - */ - public $name; + // model name + public string $name; - /** - * @var string table name. (without brackets and db prefix) - */ - public $tableName; + // table name. (without brackets and db prefix) + public string $tableName; + + // description from the schema. + public string $description; /** - * @var string description from the schema. + * @var array|Attribute[] model attributes. */ - public $description; + public array $attributes = []; /** - * @var array|\cebe\yii2openapi\lib\items\Attribute[] model attributes. + * @var array|AttributeRelation[] database relations. */ - public $attributes = []; + public array $relations = []; /** - * @var array|\cebe\yii2openapi\lib\items\AttributeRelation[] database relations. + * @var array|NonDbRelation[] non-db relations */ - public $relations = []; + public array $nonDbRelations = []; - /*** - * @var array|\cebe\yii2openapi\lib\items\NonDbRelation[] non-db relations + /** + * @var array|ManyToManyRelation[] many-to-many relations. */ - public $nonDbRelations = []; + public array $many2many = []; /** - * @var array|\cebe\yii2openapi\lib\items\ManyToManyRelation[] many to many relations. + * @var array|AttributeRelation[] inverse relations */ - public $many2many = []; + public array $inverseRelations = []; - public $junctionCols = []; + public array $junctionCols = []; /** - * @var \cebe\yii2openapi\lib\items\DbIndex[]|array + * @var DbIndex[]|array */ - public $indexes = []; + public array $indexes = []; - public $isNotDb = false; + public bool $isNotDb = false; - public function getTableAlias():string + public function getTableAlias(): string { return '{{%' . $this->tableName . '}}'; } - public function getClassName():string + public function getClassName(): string { return Inflector::id2camel($this->name, '_'); } - public function getValidationRules():string + /** + * @throws InvalidConfigException + */ + public function getValidationRules(): string { $rules = Yii::createObject(ValidationRulesBuilder::class, [$this])->build(); $rules = array_map('strval', $rules); @@ -105,9 +107,9 @@ public function getValidationRules():string } /** - * @return \cebe\yii2openapi\lib\items\AttributeRelation[]|array + * @return AttributeRelation[]|array */ - public function getHasOneRelations():array + public function getHasOneRelations(): array { return array_filter( $this->relations, @@ -117,7 +119,7 @@ static function (AttributeRelation $relation) { ); } - public function getPkAttribute():Attribute + public function getPkAttribute(): Attribute { return $this->attributes[$this->pkName]; } @@ -125,7 +127,7 @@ public function getPkAttribute():Attribute /** * @return ColumnSchema[] */ - public function attributesToColumnSchema():array + public function attributesToColumnSchema(): array { return $this->isNotDb ? [] @@ -142,9 +144,9 @@ static function ($acc, Attribute $attribute) { } /** - * @return array|\cebe\yii2openapi\lib\items\Attribute[] + * @return array|Attribute[] */ - public function getEnumAttributes():array + public function getEnumAttributes(): array { return array_filter( $this->attributes, @@ -155,9 +157,9 @@ static function (Attribute $attribute) { } /** - * @return array|\cebe\yii2openapi\lib\items\Attribute[] + * @return array|Attribute[] */ - public function virtualAttributes():array + public function virtualAttributes(): array { return array_filter($this->attributes, static function (Attribute $attribute) { return $attribute->isVirtual; @@ -165,9 +167,9 @@ public function virtualAttributes():array } /** - * @return array|\cebe\yii2openapi\lib\items\Attribute[] + * @return array|Attribute[] */ - public function dbAttributes():array + public function dbAttributes(): array { return array_filter($this->attributes, static function (Attribute $attribute) { return !$attribute->isVirtual; diff --git a/tests/fixtures/blog.php b/tests/fixtures/blog.php index 74b04000..5d6d0b49 100644 --- a/tests/fixtures/blog.php +++ b/tests/fixtures/blog.php @@ -48,7 +48,7 @@ ->setRequired()->setDefault(false)->setFakerStub('$faker->boolean'), ], 'relations' => [ - 'posts' => new AttributeRelation('posts', 'blog_posts', 'Post', 'hasMany', ['category_id' => 'id']), + 'posts' => (new AttributeRelation('posts', 'blog_posts', 'Post', 'hasMany', ['category_id' => 'id']))->setInverse('category'), ], 'indexes' => [ 'categories_active_index' => DbIndex::make('categories', ['active']), @@ -88,7 +88,7 @@ 'hasOne', ['id' => 'category_id']), 'created_by' => new AttributeRelation('created_by', 'users', 'User', 'hasOne', ['id' => 'created_by_id']), - 'comments' => new AttributeRelation('comments', 'post_comments', 'Comment', 'hasMany', ['post_id' => 'uid']), + 'comments' => (new AttributeRelation('comments', 'post_comments', 'Comment', 'hasMany', ['post_id' => 'uid']))->setInverse('post'), ], 'indexes' => [ 'blog_posts_title_key' => DbIndex::make('blog_posts', ['title'], null, true), diff --git a/tests/fixtures/non-db.php b/tests/fixtures/non-db.php index 220a486d..0e4fb69d 100644 --- a/tests/fixtures/non-db.php +++ b/tests/fixtures/non-db.php @@ -25,7 +25,7 @@ ], 'relations' => [ 'parentPet' => new AttributeRelation('parentPet', 'pets', 'Pet', 'hasOne', ['id' => 'parentPet_id']), - 'favoritePets' => new AttributeRelation('favoritePets', 'pets', 'Pet', 'hasMany', ['pet_statistic_id' => 'id']), + 'favoritePets' => (new AttributeRelation('favoritePets', 'pets', 'Pet', 'hasMany', ['pet_statistic_id' => 'id']))->setInverse('petStatistic'), ], 'nonDbRelations' => [ 'topDoctors' => new NonDbRelation('topDoctors', 'Doctor', 'hasMany'), diff --git a/tests/specs/blog/models/base/Category.php b/tests/specs/blog/models/base/Category.php index f22e0014..fc11c09b 100644 --- a/tests/specs/blog/models/base/Category.php +++ b/tests/specs/blog/models/base/Category.php @@ -32,6 +32,11 @@ public function rules() public function getPosts() { - return $this->hasMany(\app\models\Post::class, ['category_id' => 'id']); + return $this->hasMany(\app\models\Post::class, ['category_id' => 'id'])->inverseOf('category'); + } + + public function getPost() + { + return $this->hasOne(\app\models\Post::class, ['category_id' => 'id'])->inverseOf('category'); } } diff --git a/tests/specs/blog/models/base/Post.php b/tests/specs/blog/models/base/Post.php index 457978b5..abe61623 100644 --- a/tests/specs/blog/models/base/Post.php +++ b/tests/specs/blog/models/base/Post.php @@ -55,6 +55,11 @@ public function getCreatedBy() public function getComments() { - return $this->hasMany(\app\models\Comment::class, ['post_id' => 'uid']); + return $this->hasMany(\app\models\Comment::class, ['post_id' => 'uid'])->inverseOf('post'); + } + + public function getComment() + { + return $this->hasOne(\app\models\Comment::class, ['post_id' => 'uid'])->inverseOf('post'); } } diff --git a/tests/specs/blog/models/base/User.php b/tests/specs/blog/models/base/User.php index f0e484e8..dc94110c 100644 --- a/tests/specs/blog/models/base/User.php +++ b/tests/specs/blog/models/base/User.php @@ -39,4 +39,14 @@ public function rules() 'created_at_datetime' => [['created_at'], 'datetime', 'format' => 'php:Y-m-d H:i:s'], ]; } + + public function getPost() + { + return $this->hasOne(\app\models\Post::class, ['created_by_id' => 'id'])->inverseOf('created_by'); + } + + public function getComment2() + { + return $this->hasOne(\app\models\Comment::class, ['author_id' => 'id'])->inverseOf('author'); + } } diff --git a/tests/specs/blog_v2/models/base/Category.php b/tests/specs/blog_v2/models/base/Category.php index a70be3ff..23ed92ea 100644 --- a/tests/specs/blog_v2/models/base/Category.php +++ b/tests/specs/blog_v2/models/base/Category.php @@ -32,6 +32,11 @@ public function rules() public function getPosts() { - return $this->hasMany(\app\models\Post::class, ['category_id' => 'id']); + return $this->hasMany(\app\models\Post::class, ['category_id' => 'id'])->inverseOf('category'); + } + + public function getPost() + { + return $this->hasOne(\app\models\Post::class, ['category_id' => 'id'])->inverseOf('category'); } } diff --git a/tests/specs/blog_v2/models/base/Post.php b/tests/specs/blog_v2/models/base/Post.php index 44fe4275..83316fab 100644 --- a/tests/specs/blog_v2/models/base/Post.php +++ b/tests/specs/blog_v2/models/base/Post.php @@ -61,7 +61,7 @@ public function getCreatedBy() public function getComments() { - return $this->hasMany(\app\models\Comment::class, ['post_id' => 'id']); + return $this->hasMany(\app\models\Comment::class, ['post_id' => 'id'])->inverseOf('post'); } public function getTags() @@ -69,4 +69,9 @@ public function getTags() return $this->hasMany(\app\models\Tag::class, ['id' => 'tag_id']) ->viaTable('posts2tags', ['post_id' => 'id']); } + + public function getComment() + { + return $this->hasOne(\app\models\Comment::class, ['post_id' => 'id'])->inverseOf('post'); + } } diff --git a/tests/specs/blog_v2/models/base/User.php b/tests/specs/blog_v2/models/base/User.php index 195b5b5e..7274ae0a 100644 --- a/tests/specs/blog_v2/models/base/User.php +++ b/tests/specs/blog_v2/models/base/User.php @@ -43,4 +43,14 @@ public function rules() 'created_at_datetime' => [['created_at'], 'datetime', 'format' => 'php:Y-m-d H:i:s'], ]; } + + public function getPost() + { + return $this->hasOne(\app\models\Post::class, ['created_by_id' => 'id'])->inverseOf('created_by'); + } + + public function getComment2() + { + return $this->hasOne(\app\models\Comment::class, ['user_id' => 'id'])->inverseOf('user'); + } } diff --git a/tests/specs/fk_col_name/app/models/base/Delivery.php b/tests/specs/fk_col_name/app/models/base/Delivery.php index ae7cbd83..0380af0e 100644 --- a/tests/specs/fk_col_name/app/models/base/Delivery.php +++ b/tests/specs/fk_col_name/app/models/base/Delivery.php @@ -23,4 +23,9 @@ public function rules() 'title_string' => [['title'], 'string'], ]; } + + public function getWebhook() + { + return $this->hasOne(\app\models\Webhook::class, ['redelivery_of' => 'id'])->inverseOf('redelivery_of'); + } } diff --git a/tests/specs/fk_col_name/app/models/base/User.php b/tests/specs/fk_col_name/app/models/base/User.php index d76c3f4d..2c9c1c06 100644 --- a/tests/specs/fk_col_name/app/models/base/User.php +++ b/tests/specs/fk_col_name/app/models/base/User.php @@ -24,4 +24,9 @@ public function rules() 'name_string' => [['name'], 'string'], ]; } + + public function getWebhook() + { + return $this->hasOne(\app\models\Webhook::class, ['user_id' => 'id'])->inverseOf('user'); + } } diff --git a/tests/specs/fk_col_name_index/app/models/base/Delivery.php b/tests/specs/fk_col_name_index/app/models/base/Delivery.php index ae7cbd83..b1f57fc1 100644 --- a/tests/specs/fk_col_name_index/app/models/base/Delivery.php +++ b/tests/specs/fk_col_name_index/app/models/base/Delivery.php @@ -23,4 +23,14 @@ public function rules() 'title_string' => [['title'], 'string'], ]; } + + public function getWebhook() + { + return $this->hasOne(\app\models\Webhook::class, ['redelivery_of' => 'id'])->inverseOf('redelivery_of'); + } + + public function getWebhook2() + { + return $this->hasOne(\app\models\Webhook::class, ['rd_abc_2' => 'id'])->inverseOf('rd2'); + } } diff --git a/tests/specs/fk_col_name_index/app/models/base/User.php b/tests/specs/fk_col_name_index/app/models/base/User.php index d76c3f4d..2c9c1c06 100644 --- a/tests/specs/fk_col_name_index/app/models/base/User.php +++ b/tests/specs/fk_col_name_index/app/models/base/User.php @@ -24,4 +24,9 @@ public function rules() 'name_string' => [['name'], 'string'], ]; } + + public function getWebhook() + { + return $this->hasOne(\app\models\Webhook::class, ['user_id' => 'id'])->inverseOf('user'); + } } diff --git a/tests/specs/issue_fix/159_bug_giiapi_generated_rules_emailid/maria/models/base/Mailing.php b/tests/specs/issue_fix/159_bug_giiapi_generated_rules_emailid/maria/models/base/Mailing.php index 77049b23..a7d5017a 100644 --- a/tests/specs/issue_fix/159_bug_giiapi_generated_rules_emailid/maria/models/base/Mailing.php +++ b/tests/specs/issue_fix/159_bug_giiapi_generated_rules_emailid/maria/models/base/Mailing.php @@ -26,4 +26,9 @@ public function rules() 'paymentMethodName_string' => [['paymentMethodName'], 'string'], ]; } + + public function getContact() + { + return $this->hasOne(\app\models\Contact::class, ['mailing_id' => 'id'])->inverseOf('mailing'); + } } diff --git a/tests/specs/issue_fix/162_bug_dollarref_with_x_faker/app/models/base/Invoice.php b/tests/specs/issue_fix/162_bug_dollarref_with_x_faker/app/models/base/Invoice.php index 061e3508..4236c8eb 100644 --- a/tests/specs/issue_fix/162_bug_dollarref_with_x_faker/app/models/base/Invoice.php +++ b/tests/specs/issue_fix/162_bug_dollarref_with_x_faker/app/models/base/Invoice.php @@ -19,4 +19,9 @@ public function rules() { return []; } + + public function getOrder() + { + return $this->hasOne(\app\models\Order::class, ['invoice_id' => 'id'])->inverseOf('invoice'); + } } diff --git a/tests/specs/issue_fix/175_bug_allof_with_multiple_dollarrefs/pgsql/models/base/Account.php b/tests/specs/issue_fix/175_bug_allof_with_multiple_dollarrefs/pgsql/models/base/Account.php index f3dae5db..3c6d28b7 100644 --- a/tests/specs/issue_fix/175_bug_allof_with_multiple_dollarrefs/pgsql/models/base/Account.php +++ b/tests/specs/issue_fix/175_bug_allof_with_multiple_dollarrefs/pgsql/models/base/Account.php @@ -26,4 +26,9 @@ public function rules() 'paymentMethodName_string' => [['paymentMethodName'], 'string'], ]; } + + public function getContact() + { + return $this->hasOne(\app\models\Contact::class, ['account_id' => 'id'])->inverseOf('account'); + } } diff --git a/tests/specs/issue_fix/25_generate_inverse_relations/index.php b/tests/specs/issue_fix/25_generate_inverse_relations/index.php new file mode 100644 index 00000000..535787af --- /dev/null +++ b/tests/specs/issue_fix/25_generate_inverse_relations/index.php @@ -0,0 +1,14 @@ + '@specs/issue_fix/25_generate_inverse_relations/index.yaml', + 'generateUrls' => false, + 'generateModels' => true, + 'excludeModels' => [ + 'Error', + ], + 'generateControllers' => false, + 'generateMigrations' => true, + 'generateModelFaker' => true, // `generateModels` must be `true` in order to use `generateModelFaker` as `true` +]; + diff --git a/tests/specs/issue_fix/25_generate_inverse_relations/index.yaml b/tests/specs/issue_fix/25_generate_inverse_relations/index.yaml new file mode 100644 index 00000000..b6f5ded3 --- /dev/null +++ b/tests/specs/issue_fix/25_generate_inverse_relations/index.yaml @@ -0,0 +1,77 @@ + +openapi: 3.0.3 + +info: + title: 'Generate inverse relations #25' + version: 1.0.0 + +components: + schemas: + User: + type: object + required: + - id + - name + properties: + id: + type: integer + readOnly: true + name: + type: string + maxLength: 128 + accounts: + type: array + items: + $ref: '#/components/schemas/Account' + + Account: + description: Account + type: object + required: + - id + - name + properties: + id: + type: integer + readOnly: true + name: + description: account name + type: string + maxLength: 128 + paymentMethodName: + type: string + user: + $ref: '#/components/schemas/User' + user2: # copy of user (not one to many) + $ref: '#/components/schemas/User' + user3: # copy of user (not one to many) + allOf: + - $ref: '#/components/schemas/User' + - x-fk-column-name: user3 + + Menu: + required: + - id + - name + properties: + id: + type: integer + format: int64 + readOnly: True + name: + type: string + maxLength: 100 + minLength: 3 + parent: + $ref: '#/components/schemas/Menu/properties/id' + childes: + type: array + items: + $ref: '#/components/schemas/Menu/properties/parent' + +paths: + '/account': + get: + responses: + '200': + description: Account with id = "\" was not found. diff --git a/tests/specs/issue_fix/25_generate_inverse_relations/mysql/migrations_mysql_db/m200000_000000_create_table_users.php b/tests/specs/issue_fix/25_generate_inverse_relations/mysql/migrations_mysql_db/m200000_000000_create_table_users.php new file mode 100644 index 00000000..d151ed34 --- /dev/null +++ b/tests/specs/issue_fix/25_generate_inverse_relations/mysql/migrations_mysql_db/m200000_000000_create_table_users.php @@ -0,0 +1,20 @@ +createTable('{{%users}}', [ + 'id' => $this->primaryKey(), + 'name' => $this->string(128)->notNull(), + ]); + } + + public function down() + { + $this->dropTable('{{%users}}'); + } +} diff --git a/tests/specs/issue_fix/25_generate_inverse_relations/mysql/migrations_mysql_db/m200000_000001_create_table_accounts.php b/tests/specs/issue_fix/25_generate_inverse_relations/mysql/migrations_mysql_db/m200000_000001_create_table_accounts.php new file mode 100644 index 00000000..f2bc7b90 --- /dev/null +++ b/tests/specs/issue_fix/25_generate_inverse_relations/mysql/migrations_mysql_db/m200000_000001_create_table_accounts.php @@ -0,0 +1,30 @@ +createTable('{{%accounts}}', [ + 'id' => $this->primaryKey(), + 'name' => $this->string(128)->notNull(), + 'paymentMethodName' => $this->text()->null(), + 'user_id' => $this->integer()->null()->defaultValue(null), + 'user2_id' => $this->integer()->null()->defaultValue(null), + 'user3' => $this->integer()->null()->defaultValue(null), + ]); + $this->addForeignKey('fk_accounts_user_id_users_id', '{{%accounts}}', 'user_id', '{{%users}}', 'id'); + $this->addForeignKey('fk_accounts_user2_id_users_id', '{{%accounts}}', 'user2_id', '{{%users}}', 'id'); + $this->addForeignKey('fk_accounts_user3_users_id', '{{%accounts}}', 'user3', '{{%users}}', 'id'); + } + + public function down() + { + $this->dropForeignKey('fk_accounts_user3_users_id', '{{%accounts}}'); + $this->dropForeignKey('fk_accounts_user2_id_users_id', '{{%accounts}}'); + $this->dropForeignKey('fk_accounts_user_id_users_id', '{{%accounts}}'); + $this->dropTable('{{%accounts}}'); + } +} diff --git a/tests/specs/issue_fix/25_generate_inverse_relations/mysql/migrations_mysql_db/m200000_000002_create_table_menus.php b/tests/specs/issue_fix/25_generate_inverse_relations/mysql/migrations_mysql_db/m200000_000002_create_table_menus.php new file mode 100644 index 00000000..78011c88 --- /dev/null +++ b/tests/specs/issue_fix/25_generate_inverse_relations/mysql/migrations_mysql_db/m200000_000002_create_table_menus.php @@ -0,0 +1,23 @@ +createTable('{{%menus}}', [ + 'id' => $this->bigPrimaryKey(), + 'name' => $this->string(100)->notNull(), + 'parent_id' => $this->bigInteger()->null()->defaultValue(null), + ]); + $this->addForeignKey('fk_menus_parent_id_menus_id', '{{%menus}}', 'parent_id', '{{%menus}}', 'id'); + } + + public function down() + { + $this->dropForeignKey('fk_menus_parent_id_menus_id', '{{%menus}}'); + $this->dropTable('{{%menus}}'); + } +} diff --git a/tests/specs/issue_fix/25_generate_inverse_relations/mysql/models/Account.php b/tests/specs/issue_fix/25_generate_inverse_relations/mysql/models/Account.php new file mode 100644 index 00000000..2d25d7fc --- /dev/null +++ b/tests/specs/issue_fix/25_generate_inverse_relations/mysql/models/Account.php @@ -0,0 +1,10 @@ +generateModels(['author_id' => 1]); + * $model = (new PostFaker())->generateModels(function($model, $faker, $uniqueFaker) { + * $model->scenario = 'create'; + * $model->author_id = 1; + * return $model; + * }); + **/ + public function generateModel($attributes = []) + { + $faker = $this->faker; + $uniqueFaker = $this->uniqueFaker; + $model = new Account(); + //$model->id = $uniqueFaker->numberBetween(0, 1000000); + $model->name = substr($faker->text(128), 0, 128); + $model->paymentMethodName = $faker->sentence; + $model->user_id = $faker->randomElement(\app\models\User::find()->select("id")->column()); + $model->user2_id = $faker->randomElement(\app\models\User::find()->select("id")->column()); + $model->user3 = $faker->randomElement(\app\models\User::find()->select("id")->column()); + if (!is_callable($attributes)) { + $model->setAttributes($attributes, false); + } else { + $model = $attributes($model, $faker, $uniqueFaker); + } + return $model; + } + + public static function dependentOn() + { + return [ + // just model class names + 'User', + + ]; + } +} diff --git a/tests/specs/issue_fix/25_generate_inverse_relations/mysql/models/BaseModelFaker.php b/tests/specs/issue_fix/25_generate_inverse_relations/mysql/models/BaseModelFaker.php new file mode 100644 index 00000000..c367fbb4 --- /dev/null +++ b/tests/specs/issue_fix/25_generate_inverse_relations/mysql/models/BaseModelFaker.php @@ -0,0 +1,144 @@ +faker = FakerFactory::create(str_replace('-', '_', \Yii::$app->language)); + $this->uniqueFaker = new UniqueGenerator($this->faker); + } + + abstract public function generateModel($attributes = []); + + public function getFaker():Generator + { + return $this->faker; + } + + public function getUniqueFaker():UniqueGenerator + { + return $this->uniqueFaker; + } + + public function setFaker(Generator $faker):void + { + $this->faker = $faker; + } + + public function setUniqueFaker(UniqueGenerator $faker):void + { + $this->uniqueFaker = $faker; + } + + /** + * Generate and return model + * @param array|callable $attributes + * @param UniqueGenerator|null $uniqueFaker + * @return \yii\db\ActiveRecord + * @example MyFaker::makeOne(['user_id' => 1, 'title' => 'foo']); + * @example MyFaker::makeOne( function($model, $faker) { + * $model->scenario = 'create'; + * $model->setAttributes(['user_id' => 1, 'title' => $faker->sentence]); + * return $model; + * }); + */ + public static function makeOne($attributes = [], ?UniqueGenerator $uniqueFaker = null) + { + $fakeBuilder = new static(); + if ($uniqueFaker !== null) { + $fakeBuilder->setUniqueFaker($uniqueFaker); + } + $model = $fakeBuilder->generateModel($attributes); + return $model; + } + + /** + * Generate, save and return model + * @param array|callable $attributes + * @param UniqueGenerator|null $uniqueFaker + * @return \yii\db\ActiveRecord + * @example MyFaker::saveOne(['user_id' => 1, 'title' => 'foo']); + * @example MyFaker::saveOne( function($model, $faker) { + * $model->scenario = 'create'; + * $model->setAttributes(['user_id' => 1, 'title' => $faker->sentence]); + * return $model; + * }); + */ + public static function saveOne($attributes = [], ?UniqueGenerator $uniqueFaker = null) + { + $model = static::makeOne($attributes, $uniqueFaker); + $model->save(); + return $model; + } + + /** + * Generate and return multiple models + * @param int $number + * @param array|callable $commonAttributes + * @return \yii\db\ActiveRecord[]|array + * @example TaskFaker::make(5, ['project_id'=>1, 'user_id' => 2]); + * @example TaskFaker::make(5, function($model, $faker, $uniqueFaker) { + * $model->setAttributes(['name' => $uniqueFaker->username, 'state'=>$faker->boolean(20)]); + * return $model; + * }); + */ + public static function make(int $number, $commonAttributes = [], ?UniqueGenerator $uniqueFaker = null):array + { + if ($number < 1) { + return []; + } + $fakeBuilder = new static(); + if ($uniqueFaker !== null) { + $fakeBuilder->setUniqueFaker($uniqueFaker); + } + return array_map(function () use ($commonAttributes, $fakeBuilder) { + $model = $fakeBuilder->generateModel($commonAttributes); + return $model; + }, range(0, $number -1)); + } + + /** + * Generate, save and return multiple models + * @param int $number + * @param array|callable $commonAttributes + * @return \yii\db\ActiveRecord[]|array + * @example TaskFaker::save(5, ['project_id'=>1, 'user_id' => 2]); + * @example TaskFaker::save(5, function($model, $faker, $uniqueFaker) { + * $model->setAttributes(['name' => $uniqueFaker->username, 'state'=>$faker->boolean(20)]); + * return $model; + * }); + */ + public static function save(int $number, $commonAttributes = [], ?UniqueGenerator $uniqueFaker = null):array + { + if ($number < 1) { + return []; + } + $fakeBuilder = new static(); + if ($uniqueFaker !== null) { + $fakeBuilder->setUniqueFaker($uniqueFaker); + } + return array_map(function () use ($commonAttributes, $fakeBuilder) { + $model = $fakeBuilder->generateModel($commonAttributes); + $model->save(); + return $model; + }, range(0, $number -1)); + } +} diff --git a/tests/specs/issue_fix/25_generate_inverse_relations/mysql/models/Menu.php b/tests/specs/issue_fix/25_generate_inverse_relations/mysql/models/Menu.php new file mode 100644 index 00000000..2b5867b4 --- /dev/null +++ b/tests/specs/issue_fix/25_generate_inverse_relations/mysql/models/Menu.php @@ -0,0 +1,10 @@ +generateModels(['author_id' => 1]); + * $model = (new PostFaker())->generateModels(function($model, $faker, $uniqueFaker) { + * $model->scenario = 'create'; + * $model->author_id = 1; + * return $model; + * }); + **/ + public function generateModel($attributes = []) + { + $faker = $this->faker; + $uniqueFaker = $this->uniqueFaker; + $model = new Menu(); + //$model->id = $uniqueFaker->numberBetween(0, 1000000); + $model->name = substr($faker->text(100), 0, 100); + $model->parent_id = $faker->randomElement(\app\models\Menu::find()->select("id")->column()); + if (!is_callable($attributes)) { + $model->setAttributes($attributes, false); + } else { + $model = $attributes($model, $faker, $uniqueFaker); + } + return $model; + } + + public static function dependentOn() + { + return [ + // just model class names + 'Menu', + + ]; + } +} diff --git a/tests/specs/issue_fix/25_generate_inverse_relations/mysql/models/User.php b/tests/specs/issue_fix/25_generate_inverse_relations/mysql/models/User.php new file mode 100644 index 00000000..9b837d6e --- /dev/null +++ b/tests/specs/issue_fix/25_generate_inverse_relations/mysql/models/User.php @@ -0,0 +1,10 @@ +generateModels(['author_id' => 1]); + * $model = (new PostFaker())->generateModels(function($model, $faker, $uniqueFaker) { + * $model->scenario = 'create'; + * $model->author_id = 1; + * return $model; + * }); + **/ + public function generateModel($attributes = []) + { + $faker = $this->faker; + $uniqueFaker = $this->uniqueFaker; + $model = new User(); + //$model->id = $uniqueFaker->numberBetween(0, 1000000); + $model->name = substr($faker->text(128), 0, 128); + if (!is_callable($attributes)) { + $model->setAttributes($attributes, false); + } else { + $model = $attributes($model, $faker, $uniqueFaker); + } + return $model; + } +} diff --git a/tests/specs/issue_fix/25_generate_inverse_relations/mysql/models/base/Account.php b/tests/specs/issue_fix/25_generate_inverse_relations/mysql/models/base/Account.php new file mode 100644 index 00000000..d0f43293 --- /dev/null +++ b/tests/specs/issue_fix/25_generate_inverse_relations/mysql/models/base/Account.php @@ -0,0 +1,56 @@ + [['name', 'paymentMethodName'], 'trim'], + 'required' => [['name'], 'required'], + 'user_id_integer' => [['user_id'], 'integer'], + 'user_id_exist' => [['user_id'], 'exist', 'targetRelation' => 'User'], + 'user2_id_integer' => [['user2_id'], 'integer'], + 'user2_id_exist' => [['user2_id'], 'exist', 'targetRelation' => 'User2'], + 'user3_integer' => [['user3'], 'integer'], + 'user3_exist' => [['user3'], 'exist', 'targetRelation' => 'User3'], + 'name_string' => [['name'], 'string', 'max' => 128], + 'paymentMethodName_string' => [['paymentMethodName'], 'string'], + ]; + } + + public function getUser() + { + return $this->hasOne(\app\models\User::class, ['id' => 'user_id']); + } + + public function getUser2() + { + return $this->hasOne(\app\models\User::class, ['id' => 'user2_id']); + } + + public function getUser3() + { + return $this->hasOne(\app\models\User::class, ['id' => 'user3']); + } +} diff --git a/tests/specs/issue_fix/25_generate_inverse_relations/mysql/models/base/Menu.php b/tests/specs/issue_fix/25_generate_inverse_relations/mysql/models/base/Menu.php new file mode 100644 index 00000000..ec3d0957 --- /dev/null +++ b/tests/specs/issue_fix/25_generate_inverse_relations/mysql/models/base/Menu.php @@ -0,0 +1,42 @@ + [['name'], 'trim'], + 'required' => [['name'], 'required'], + 'parent_id_integer' => [['parent_id'], 'integer'], + 'parent_id_exist' => [['parent_id'], 'exist', 'targetRelation' => 'Parent'], + 'name_string' => [['name'], 'string', 'min' => 3, 'max' => 100], + ]; + } + + public function getParent() + { + return $this->hasOne(\app\models\Menu::class, ['id' => 'parent_id']); + } + + public function getChildes() + { + return $this->hasMany(\app\models\Menu::class, ['parent_id' => 'id']); + } +} diff --git a/tests/specs/issue_fix/25_generate_inverse_relations/mysql/models/base/User.php b/tests/specs/issue_fix/25_generate_inverse_relations/mysql/models/base/User.php new file mode 100644 index 00000000..5f70ce96 --- /dev/null +++ b/tests/specs/issue_fix/25_generate_inverse_relations/mysql/models/base/User.php @@ -0,0 +1,48 @@ + [['name'], 'trim'], + 'required' => [['name'], 'required'], + 'name_string' => [['name'], 'string', 'max' => 128], + ]; + } + + public function getAccounts() + { + return $this->hasMany(\app\models\Account::class, ['user_id' => 'id'])->inverseOf('user'); + } + + public function getAccount() + { + return $this->hasOne(\app\models\Account::class, ['user_id' => 'id'])->inverseOf('user'); + } + + public function getAccount2() + { + return $this->hasOne(\app\models\Account::class, ['user2_id' => 'id'])->inverseOf('user2'); + } + + public function getAccount3() + { + return $this->hasOne(\app\models\Account::class, ['user3' => 'id'])->inverseOf('user3'); + } +} diff --git a/tests/specs/issue_fix/model_name_more_than_once_in_faker_148/app/models/base/Account.php b/tests/specs/issue_fix/model_name_more_than_once_in_faker_148/app/models/base/Account.php index 0ee4adc0..c3125556 100644 --- a/tests/specs/issue_fix/model_name_more_than_once_in_faker_148/app/models/base/Account.php +++ b/tests/specs/issue_fix/model_name_more_than_once_in_faker_148/app/models/base/Account.php @@ -24,4 +24,19 @@ public function rules() 'name_string' => [['name'], 'string', 'max' => 40], ]; } + + public function getE123() + { + return $this->hasOne(\app\models\E123::class, ['account_id' => 'id'])->inverseOf('account'); + } + + public function getE1232() + { + return $this->hasOne(\app\models\E123::class, ['account_2_id' => 'id'])->inverseOf('account_2'); + } + + public function getE1233() + { + return $this->hasOne(\app\models\E123::class, ['account_3_id' => 'id'])->inverseOf('account_3'); + } } diff --git a/tests/specs/petstore/models/base/Store.php b/tests/specs/petstore/models/base/Store.php index b1f6bc0f..5e6696af 100644 --- a/tests/specs/petstore/models/base/Store.php +++ b/tests/specs/petstore/models/base/Store.php @@ -24,4 +24,9 @@ public function rules() 'name_string' => [['name'], 'string'], ]; } + + public function getPet() + { + return $this->hasOne(\app\models\Pet::class, ['store_id' => 'id'])->inverseOf('store'); + } } diff --git a/tests/specs/petstore_jsonapi/models/base/Pet.php b/tests/specs/petstore_jsonapi/models/base/Pet.php index f0e3138a..251a39a4 100644 --- a/tests/specs/petstore_jsonapi/models/base/Pet.php +++ b/tests/specs/petstore_jsonapi/models/base/Pet.php @@ -59,4 +59,9 @@ public function getDuplicates() { return $this->hasMany(\app\models\Pet::class, ['tag' => 'tag']); } + + public function getPetStatistic() + { + return $this->hasOne(\app\models\PetStatistic::class, ['parentPet_id' => 'id'])->inverseOf('parentPet'); + } } diff --git a/tests/specs/petstore_namespace/mymodels/base/Store.php b/tests/specs/petstore_namespace/mymodels/base/Store.php index 6ecc3e47..fa304857 100644 --- a/tests/specs/petstore_namespace/mymodels/base/Store.php +++ b/tests/specs/petstore_namespace/mymodels/base/Store.php @@ -24,4 +24,9 @@ public function rules() 'name_string' => [['name'], 'string'], ]; } + + public function getPet() + { + return $this->hasOne(\app\mymodels\Pet::class, ['store_id' => 'id'])->inverseOf('store'); + } } diff --git a/tests/specs/relations_in_faker/app/models/base/A123.php b/tests/specs/relations_in_faker/app/models/base/A123.php index 9836dc3b..7b604bad 100644 --- a/tests/specs/relations_in_faker/app/models/base/A123.php +++ b/tests/specs/relations_in_faker/app/models/base/A123.php @@ -32,4 +32,9 @@ public function getB123() { return $this->hasOne(\app\models\B123::class, ['id' => 'b123_id']); } + + public function getRouting() + { + return $this->hasOne(\app\models\Routing::class, ['a123_id' => 'id'])->inverseOf('a123'); + } } diff --git a/tests/specs/relations_in_faker/app/models/base/Account.php b/tests/specs/relations_in_faker/app/models/base/Account.php index 0ee4adc0..07d561cd 100644 --- a/tests/specs/relations_in_faker/app/models/base/Account.php +++ b/tests/specs/relations_in_faker/app/models/base/Account.php @@ -24,4 +24,9 @@ public function rules() 'name_string' => [['name'], 'string', 'max' => 40], ]; } + + public function getDomain() + { + return $this->hasOne(\app\models\Domain::class, ['account_id' => 'id'])->inverseOf('account'); + } } diff --git a/tests/specs/relations_in_faker/app/models/base/B123.php b/tests/specs/relations_in_faker/app/models/base/B123.php index f05fe320..2fdde13d 100644 --- a/tests/specs/relations_in_faker/app/models/base/B123.php +++ b/tests/specs/relations_in_faker/app/models/base/B123.php @@ -32,4 +32,14 @@ public function getC123() { return $this->hasOne(\app\models\C123::class, ['id' => 'c123_id']); } + + public function getA123() + { + return $this->hasOne(\app\models\A123::class, ['b123_id' => 'id'])->inverseOf('b123'); + } + + public function getE1232() + { + return $this->hasOne(\app\models\E123::class, ['b123_id' => 'id'])->inverseOf('b123'); + } } diff --git a/tests/specs/relations_in_faker/app/models/base/C123.php b/tests/specs/relations_in_faker/app/models/base/C123.php index faa3f1e5..7e074ea9 100644 --- a/tests/specs/relations_in_faker/app/models/base/C123.php +++ b/tests/specs/relations_in_faker/app/models/base/C123.php @@ -23,4 +23,9 @@ public function rules() 'name_string' => [['name'], 'string'], ]; } + + public function getB123() + { + return $this->hasOne(\app\models\B123::class, ['c123_id' => 'id'])->inverseOf('c123'); + } } diff --git a/tests/specs/relations_in_faker/app/models/base/D123.php b/tests/specs/relations_in_faker/app/models/base/D123.php index a6050a8a..7cad2f34 100644 --- a/tests/specs/relations_in_faker/app/models/base/D123.php +++ b/tests/specs/relations_in_faker/app/models/base/D123.php @@ -23,4 +23,9 @@ public function rules() 'name_string' => [['name'], 'string'], ]; } + + public function getRouting() + { + return $this->hasOne(\app\models\Routing::class, ['d123_id' => 'id'])->inverseOf('d123'); + } } diff --git a/tests/specs/relations_in_faker/app/models/base/Domain.php b/tests/specs/relations_in_faker/app/models/base/Domain.php index 3f861f9e..b7a807e1 100644 --- a/tests/specs/relations_in_faker/app/models/base/Domain.php +++ b/tests/specs/relations_in_faker/app/models/base/Domain.php @@ -38,6 +38,11 @@ public function getAccount() public function getRoutings() { - return $this->hasMany(\app\models\Routing::class, ['domain_id' => 'id']); + return $this->hasMany(\app\models\Routing::class, ['domain_id' => 'id'])->inverseOf('domain'); + } + + public function getRouting() + { + return $this->hasOne(\app\models\Routing::class, ['domain_id' => 'id'])->inverseOf('domain'); } } diff --git a/tests/specs/relations_in_faker/app/models/base/Routing.php b/tests/specs/relations_in_faker/app/models/base/Routing.php index 71a31b81..2aa9f710 100644 --- a/tests/specs/relations_in_faker/app/models/base/Routing.php +++ b/tests/specs/relations_in_faker/app/models/base/Routing.php @@ -3,7 +3,7 @@ namespace app\models\base; /** - * rounting specification + * routing specification * * @property int $id * @property int $domain_id domain diff --git a/tests/specs/relations_in_faker/relations_in_faker.yaml b/tests/specs/relations_in_faker/relations_in_faker.yaml index 65853bbd..546e9b1a 100644 --- a/tests/specs/relations_in_faker/relations_in_faker.yaml +++ b/tests/specs/relations_in_faker/relations_in_faker.yaml @@ -64,7 +64,7 @@ components: nullable: false Routing: - description: rounting specification + description: routing specification type: object required: - id diff --git a/tests/unit/IssueFixTest.php b/tests/unit/IssueFixTest.php index b6c7abdb..6af7e11b 100644 --- a/tests/unit/IssueFixTest.php +++ b/tests/unit/IssueFixTest.php @@ -360,4 +360,19 @@ public function test158BugGiiapiGeneratedRulesEnumWithTrim() ]); $this->checkFiles($actualFiles, $expectedFiles); } + + // https://github.com/php-openapi/yii2-openapi/issues/25 + public function test25GenerateInverseRelations() + { + $testFile = Yii::getAlias("@specs/issue_fix/25_generate_inverse_relations/index.php"); + $this->runGenerator($testFile); + $this->runActualMigrations('mysql', 3); + $actualFiles = FileHelper::findFiles(Yii::getAlias('@app'), [ + 'recursive' => true, + ]); + $expectedFiles = FileHelper::findFiles(Yii::getAlias("@specs/issue_fix/25_generate_inverse_relations/mysql"), [ + 'recursive' => true, + ]); + $this->checkFiles($actualFiles, $expectedFiles); + } }