summaryrefslogtreecommitdiffstats
path: root/vendor/ipl/orm/src/Relation
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--vendor/ipl/orm/src/Relation.php336
-rw-r--r--vendor/ipl/orm/src/Relation/BelongsTo.php13
-rw-r--r--vendor/ipl/orm/src/Relation/BelongsToMany.php199
-rw-r--r--vendor/ipl/orm/src/Relation/HasMany.php13
-rw-r--r--vendor/ipl/orm/src/Relation/HasOne.php12
-rw-r--r--vendor/ipl/orm/src/Relation/Junction.php43
-rw-r--r--vendor/ipl/orm/src/Relations.php212
7 files changed, 828 insertions, 0 deletions
diff --git a/vendor/ipl/orm/src/Relation.php b/vendor/ipl/orm/src/Relation.php
new file mode 100644
index 0000000..1959363
--- /dev/null
+++ b/vendor/ipl/orm/src/Relation.php
@@ -0,0 +1,336 @@
+<?php
+
+namespace ipl\Orm;
+
+use function ipl\Stdlib\get_php_type;
+
+/**
+ * Relations represent the connection between models, i.e. the association between rows in one or more tables
+ * on the basis of matching key columns. The relationships are defined using candidate key-foreign key constructs.
+ */
+class Relation
+{
+ /** @var string Name of the relation */
+ protected $name;
+
+ /** @var Model Source model */
+ protected $source;
+
+ /** @var string|array Column name(s) of the foreign key found in the target table */
+ protected $foreignKey;
+
+ /** @var string|array Column name(s) of the candidate key in the source table which references the foreign key */
+ protected $candidateKey;
+
+ /** @var string Target model class */
+ protected $targetClass;
+
+ /** @var Model Target model */
+ protected $target;
+
+ /** @var string Type of the JOIN used in the query */
+ protected $joinType = 'INNER';
+
+ /** @var bool Whether this is the inverse of a relationship */
+ protected $inverse;
+
+ /** @var bool Whether this is a to-one relationship */
+ protected $isOne = true;
+
+ /**
+ * Get the default column name(s) in the source table used to match the foreign key
+ *
+ * The default candidate key is the primary key column name(s) of the given model.
+ *
+ * @param Model $source
+ *
+ * @return array
+ */
+ public static function getDefaultCandidateKey(Model $source)
+ {
+ return (array) $source->getKeyName();
+ }
+
+ /**
+ * Get the default column name(s) of the foreign key found in the target table
+ *
+ * The default foreign key is the given model's primary key column name(s) prefixed with its table name.
+ *
+ * @param Model $source
+ *
+ * @return array
+ */
+ public static function getDefaultForeignKey(Model $source)
+ {
+ $tableName = $source->getTableName();
+
+ return array_map(
+ function ($key) use ($tableName) {
+ return "{$tableName}_{$key}";
+ },
+ (array) $source->getKeyName()
+ );
+ }
+
+ /**
+ * Get whether this is a to-one relationship
+ *
+ * @return bool
+ */
+ public function isOne()
+ {
+ return $this->isOne;
+ }
+
+ /**
+ * Get the name of the relation
+ *
+ * @return string
+ */
+ public function getName()
+ {
+ return $this->name;
+ }
+
+ /**
+ * Set the name of the relation
+ *
+ * @param string $name
+ *
+ * @return $this
+ */
+ public function setName($name)
+ {
+ $this->name = $name;
+
+ return $this;
+ }
+
+ /**
+ * Get the source model of the relation
+ *
+ * @return Model
+ */
+ public function getSource()
+ {
+ return $this->source;
+ }
+
+ /**
+ * Set the source model of the relation
+ *
+ * @param Model $source
+ *
+ * @return $this
+ */
+ public function setSource(Model $source)
+ {
+ $this->source = $source;
+
+ return $this;
+ }
+
+ /**
+ * Get the column name(s) of the foreign key found in the target table
+ *
+ * @return string|array Array if the foreign key is compound, string otherwise
+ */
+ public function getForeignKey()
+ {
+ return $this->foreignKey;
+ }
+
+ /**
+ * Set the column name(s) of the foreign key found in the target table
+ *
+ * @param string|array $foreignKey Array if the foreign key is compound, string otherwise
+ *
+ * @return $this
+ */
+ public function setForeignKey($foreignKey)
+ {
+ $this->foreignKey = $foreignKey;
+
+ return $this;
+ }
+
+ /**
+ * Get the column name(s) of the candidate key in the source table which references the foreign key
+ *
+ * @return string|array Array if the candidate key is compound, string otherwise
+ */
+ public function getCandidateKey()
+ {
+ return $this->candidateKey;
+ }
+
+ /**
+ * Set the column name(s) of the candidate key in the source table which references the foreign key
+ *
+ * @param string|array $candidateKey Array if the candidate key is compound, string otherwise
+ *
+ * @return $this
+ */
+ public function setCandidateKey($candidateKey)
+ {
+ $this->candidateKey = $candidateKey;
+
+ return $this;
+ }
+
+ /**
+ * Get the target model class
+ *
+ * @return string
+ */
+ public function getTargetClass()
+ {
+ return $this->targetClass;
+ }
+
+ /**
+ * Set the target model class
+ *
+ * @param string $targetClass
+ *
+ * @return $this
+ *
+ * @throws \InvalidArgumentException If the target model class is not of type string
+ */
+ public function setTargetClass($targetClass)
+ {
+ if (! is_string($targetClass)) {
+ // Require a class name here instead of a concrete model in oder to prevent circular references when
+ // constructing relations
+ throw new \InvalidArgumentException(sprintf(
+ '%s() expects parameter 1 to be string, %s given',
+ __METHOD__,
+ get_php_type($targetClass)
+ ));
+ }
+
+ $this->targetClass = $targetClass;
+
+ return $this;
+ }
+
+ /**
+ * Get the target model
+ *
+ * Returns the model from {@link setTarget()} or an instance of {@link getTargetClass()}.
+ * Note that multiple calls to this method always returns the very same model instance.
+ *
+ * @return Model
+ */
+ public function getTarget()
+ {
+ if ($this->target === null) {
+ $targetClass = $this->getTargetClass();
+ $this->target = new $targetClass();
+ }
+
+ return $this->target;
+ }
+
+ /**
+ * Set the the target model
+ *
+ * @param Model $target
+ *
+ * @return $this
+ */
+ public function setTarget(Model $target)
+ {
+ $this->target = $target;
+
+ return $this;
+ }
+
+ /**
+ * Get the type of the JOIN used in the query
+ *
+ * @return string
+ */
+ public function getJoinType()
+ {
+ return $this->joinType;
+ }
+
+ /**
+ * Set the type of the JOIN used in the query
+ *
+ * @param string $joinType
+ *
+ * @return Relation
+ */
+ public function setJoinType($joinType)
+ {
+ $this->joinType = $joinType;
+
+ return $this;
+ }
+
+ /**
+ * Determine the candidate key-foreign key construct of the relation
+ *
+ * @param Model $source
+ *
+ * @return array Candidate key-foreign key column name pairs
+ *
+ * @throws \UnexpectedValueException If there's no candidate key to be found
+ * or the foreign key count does not match the candidate key count
+ */
+ public function determineKeys(Model $source)
+ {
+ $candidateKey = (array) $this->getCandidateKey();
+
+ if (empty($candidateKey)) {
+ $candidateKey = $this->inverse
+ ? static::getDefaultForeignKey($this->getTarget())
+ : static::getDefaultCandidateKey($source);
+ }
+
+ if (empty($candidateKey)) {
+ throw new \UnexpectedValueException(sprintf(
+ "Can't join relation '%s' in model '%s'. No candidate key found.",
+ $this->getName(),
+ get_class($source)
+ ));
+ }
+
+ $foreignKey = (array) $this->getForeignKey();
+
+ if (empty($foreignKey)) {
+ $foreignKey = $this->inverse
+ ? static::getDefaultCandidateKey($this->getTarget())
+ : static::getDefaultForeignKey($source);
+ }
+
+ if (count($foreignKey) !== count($candidateKey)) {
+ throw new \UnexpectedValueException(sprintf(
+ "Can't join relation '%s' in model '%s'."
+ . " Foreign key count (%s) does not match candidate key count (%s).",
+ $this->getName(),
+ get_class($source),
+ implode(', ', $foreignKey),
+ implode(', ', $candidateKey)
+ ));
+ }
+
+ return array_combine($foreignKey, $candidateKey);
+ }
+
+ /**
+ * Resolve the relation
+ *
+ * Yields a three-element array consisting of the source model, target model and the join keys.
+ *
+ * @return \Generator
+ */
+ public function resolve()
+ {
+ $source = $this->getSource();
+
+ yield [$source, $this->getTarget(), $this->determineKeys($source)];
+ }
+}
diff --git a/vendor/ipl/orm/src/Relation/BelongsTo.php b/vendor/ipl/orm/src/Relation/BelongsTo.php
new file mode 100644
index 0000000..1edcff3
--- /dev/null
+++ b/vendor/ipl/orm/src/Relation/BelongsTo.php
@@ -0,0 +1,13 @@
+<?php
+
+namespace ipl\Orm\Relation;
+
+use ipl\Orm\Relation;
+
+/**
+ * Inverse of a one-to-one or one-to-many relationship
+ */
+class BelongsTo extends Relation
+{
+ protected $inverse = true;
+}
diff --git a/vendor/ipl/orm/src/Relation/BelongsToMany.php b/vendor/ipl/orm/src/Relation/BelongsToMany.php
new file mode 100644
index 0000000..132fe55
--- /dev/null
+++ b/vendor/ipl/orm/src/Relation/BelongsToMany.php
@@ -0,0 +1,199 @@
+<?php
+
+namespace ipl\Orm\Relation;
+
+use ipl\Orm\Model;
+use ipl\Orm\Relation;
+use ipl\Orm\Relations;
+
+use function ipl\Stdlib\get_php_type;
+
+/**
+ * Many-to-many relationship
+ */
+class BelongsToMany extends Relation
+{
+ protected $isOne = false;
+
+ /** @var string Name of the join table or junction model class */
+ protected $throughClass;
+
+ /** @var Model The junction model */
+ protected $through;
+
+ /** @var string|array Column name(s) of the target model's foreign key found in the join table */
+ protected $targetForeignKey;
+
+ /** @var string|array Candidate key column name(s) in the target table which references the target foreign key */
+ protected $targetCandidateKey;
+
+ /**
+ * Get the name of the join table or junction model class
+ *
+ * @return string
+ */
+ public function getThroughClass()
+ {
+ return $this->throughClass;
+ }
+
+ /**
+ * Set the join table name or junction model class
+ *
+ * @param string $through
+ *
+ * @return $this
+ */
+ public function through($through)
+ {
+ $this->throughClass = $through;
+
+ return $this;
+ }
+
+ /**
+ * Get the junction model
+ *
+ * @return Model|Junction
+ */
+ public function getThrough()
+ {
+ if ($this->through === null) {
+ $throughClass = $this->getThroughClass();
+
+ if (class_exists($throughClass)) {
+ $this->through = new $throughClass();
+ } else {
+ $this->through = (new Junction())
+ ->setTableName($throughClass);
+ }
+ }
+
+ return $this->through;
+ }
+
+ /**
+ * Set the junction model
+ *
+ * @param Model $through
+ *
+ * @return $this
+ */
+ public function setThrough($through)
+ {
+ $this->through = $through;
+
+ return $this;
+ }
+
+ /**
+ * Get the column name(s) of the target model's foreign key found in the join table
+ *
+ * @return string|array Array if the foreign key is compound, string otherwise
+ */
+ public function getTargetForeignKey()
+ {
+ return $this->targetForeignKey;
+ }
+
+ /**
+ * Set the column name(s) of the target model's foreign key found in the join table
+ *
+ * @param string|array $targetForeignKey Array if the foreign key is compound, string otherwise
+ *
+ * @return $this
+ */
+ public function setTargetForeignKey($targetForeignKey)
+ {
+ $this->targetForeignKey = $targetForeignKey;
+
+ return $this;
+ }
+
+ /**
+ * Get the candidate key column name(s) in the target table which references the target foreign key
+ *
+ * @return string|array Array if the foreign key is compound, string otherwise
+ */
+ public function getTargetCandidateKey()
+ {
+ return $this->targetCandidateKey;
+ }
+
+ /**
+ * Set the candidate key column name(s) in the target table which references the target foreign key
+ *
+ * @param string|array $targetCandidateKey Array if the foreign key is compound, string otherwise
+ *
+ * @return $this
+ */
+ public function setTargetCandidateKey($targetCandidateKey)
+ {
+ $this->targetCandidateKey = $targetCandidateKey;
+
+ return $this;
+ }
+
+ public function resolve()
+ {
+ $source = $this->getSource();
+
+ $possibleCandidateKey = [$this->getCandidateKey()];
+ $possibleForeignKey = [$this->getForeignKey()];
+
+ $target = $this->getTarget();
+
+ $possibleTargetCandidateKey = [$this->getTargetForeignKey() ?: static::getDefaultForeignKey($target)];
+ $possibleTargetForeignKey = [$this->getTargetCandidateKey() ?: static::getDefaultCandidateKey($target)];
+
+ $junction = $this->getThrough();
+
+ if (! $junction instanceof Junction) {
+ $relations = new Relations();
+ $junction->createRelations($relations);
+
+ if ($relations->has($source->getTableName())) {
+ $sourceRelation = $relations->get($source->getTableName());
+
+ $possibleCandidateKey[] = $sourceRelation->getForeignKey();
+ $possibleForeignKey[] = $sourceRelation->getCandidateKey();
+ }
+
+ if ($relations->has($target->getTableName())) {
+ $targetRelation = $relations->get($target->getTableName());
+
+ $possibleTargetCandidateKey[] = $targetRelation->getCandidateKey();
+ $possibleTargetForeignKey[] = $targetRelation->getForeignKey();
+ }
+ }
+
+ $toJunction = (new HasMany())
+ ->setName($junction->getTableName())
+ ->setSource($source)
+ ->setTarget($junction)
+ ->setCandidateKey($this->extractKey($possibleCandidateKey))
+ ->setForeignKey($this->extractKey($possibleForeignKey));
+
+ $toTarget = (new HasMany())
+ ->setName($this->getName())
+ ->setSource($junction)
+ ->setTarget($target)
+ ->setCandidateKey($this->extractKey($possibleTargetCandidateKey))
+ ->setForeignKey($this->extractKey($possibleTargetForeignKey));
+
+ foreach ($toJunction->resolve() as $k => $v) {
+ yield $k => $v;
+ }
+
+ foreach ($toTarget->resolve() as $k => $v) {
+ yield $k => $v;
+ }
+ }
+
+ protected function extractKey(array $possibleKey)
+ {
+ $filtered = array_filter($possibleKey);
+
+ return array_pop($filtered);
+ }
+}
diff --git a/vendor/ipl/orm/src/Relation/HasMany.php b/vendor/ipl/orm/src/Relation/HasMany.php
new file mode 100644
index 0000000..3e71e25
--- /dev/null
+++ b/vendor/ipl/orm/src/Relation/HasMany.php
@@ -0,0 +1,13 @@
+<?php
+
+namespace ipl\Orm\Relation;
+
+use ipl\Orm\Relation;
+
+/**
+ * One-to-many relationship
+ */
+class HasMany extends Relation
+{
+ protected $isOne = false;
+}
diff --git a/vendor/ipl/orm/src/Relation/HasOne.php b/vendor/ipl/orm/src/Relation/HasOne.php
new file mode 100644
index 0000000..8f7a802
--- /dev/null
+++ b/vendor/ipl/orm/src/Relation/HasOne.php
@@ -0,0 +1,12 @@
+<?php
+
+namespace ipl\Orm\Relation;
+
+use ipl\Orm\Relation;
+
+/**
+ * One-to-one relationship
+ */
+class HasOne extends Relation
+{
+}
diff --git a/vendor/ipl/orm/src/Relation/Junction.php b/vendor/ipl/orm/src/Relation/Junction.php
new file mode 100644
index 0000000..9e23bb2
--- /dev/null
+++ b/vendor/ipl/orm/src/Relation/Junction.php
@@ -0,0 +1,43 @@
+<?php
+
+namespace ipl\Orm\Relation;
+
+use ipl\Orm\Model;
+
+/**
+ * Junction model for many-to-many relations
+ */
+class Junction extends Model
+{
+ /** @var string */
+ protected $tableName;
+
+ public function getTableName()
+ {
+ return $this->tableName;
+ }
+
+ /**
+ * Set the table name
+ *
+ * @param string $tableName
+ *
+ * @return $this
+ */
+ public function setTableName($tableName)
+ {
+ $this->tableName = $tableName;
+
+ return $this;
+ }
+
+ public function getKeyName()
+ {
+ return null;
+ }
+
+ public function getColumns()
+ {
+ return null;
+ }
+}
diff --git a/vendor/ipl/orm/src/Relations.php b/vendor/ipl/orm/src/Relations.php
new file mode 100644
index 0000000..340435f
--- /dev/null
+++ b/vendor/ipl/orm/src/Relations.php
@@ -0,0 +1,212 @@
+<?php
+
+namespace ipl\Orm;
+
+use ArrayIterator;
+use ipl\Orm\Exception\InvalidRelationException;
+use ipl\Orm\Relation\BelongsTo;
+use ipl\Orm\Relation\BelongsToMany;
+use ipl\Orm\Relation\HasMany;
+use ipl\Orm\Relation\HasOne;
+use IteratorAggregate;
+use Traversable;
+
+use function ipl\Stdlib\get_php_type;
+
+/**
+ * Collection of a model's relations.
+ */
+class Relations implements IteratorAggregate
+{
+ /** @var Relation[] */
+ protected $relations = [];
+
+ /**
+ * Get whether a relation with the given name exists
+ *
+ * @param string $name
+ *
+ * @return bool
+ */
+ public function has($name)
+ {
+ return isset($this->relations[$name]);
+ }
+
+ /**
+ * Get the relation with the given name
+ *
+ * @param string $name
+ *
+ * @return Relation
+ *
+ * @throws \InvalidArgumentException If the relation with the given name does not exist
+ */
+ public function get($name)
+ {
+ $this->assertRelationExists($name);
+
+ return $this->relations[$name];
+ }
+
+ /**
+ * Add the given relation to the collection
+ *
+ * @param Relation $relation
+ *
+ * @return $this
+ *
+ * @throws \InvalidArgumentException If a relation with the given name already exists
+ */
+ public function add(Relation $relation)
+ {
+ $name = $relation->getName();
+
+ $this->assertRelationDoesNotYetExist($name);
+
+ $this->relations[$name] = $relation;
+
+ return $this;
+ }
+
+ /**
+ * Create a new relation from the given class, name and target model class
+ *
+ * @param string $class Class of the relation to create
+ * @param string $name Name of the relation
+ * @param string $targetClass Target model class
+ *
+ * @return BelongsTo|BelongsToMany|HasMany|HasOne|Relation
+ *
+ * @throws \InvalidArgumentException If the target model class is not of type string
+ */
+ public function create($class, $name, $targetClass)
+ {
+ $relation = new $class();
+
+ if (! $relation instanceof Relation) {
+ throw new \InvalidArgumentException(sprintf(
+ '%s() expects parameter 1 to be a subclass of %s, %s given',
+ __METHOD__,
+ Relation::class,
+ get_php_type($relation)
+ ));
+ }
+
+ // Test target model
+ $target = new $targetClass();
+ if (! $target instanceof Model) {
+ throw new \InvalidArgumentException(sprintf(
+ '%s() expects parameter 3 to be a subclass of %s, %s given',
+ __METHOD__,
+ Model::class,
+ get_php_type($target)
+ ));
+ }
+
+ /** @var Relation $relation */
+ $relation
+ ->setName($name)
+ ->setTarget($target)
+ ->setTargetClass($targetClass);
+
+ return $relation;
+ }
+
+ /**
+ * Define a one-to-one relationship
+ *
+ * @param string $name Name of the relation
+ * @param string $targetClass Target model class
+ *
+ * @return HasOne
+ */
+ public function hasOne($name, $targetClass)
+ {
+ $relation = $this->create(HasOne::class, $name, $targetClass);
+
+ $this->add($relation);
+
+ return $relation;
+ }
+
+ /**
+ * Define a one-to-many relationship
+ *
+ * @param string $name Name of the relation
+ * @param string $targetClass Target model class
+ *
+ * @return HasMany
+ */
+ public function hasMany($name, $targetClass)
+ {
+ $relation = $this->create(HasMany::class, $name, $targetClass);
+
+ $this->add($relation);
+
+ return $relation;
+ }
+
+ /**
+ * Define the inverse of a one-to-one or one-to-many relationship
+ *
+ * @param string $name Name of the relation
+ * @param string $targetClass Target model class
+ *
+ * @return BelongsTo
+ */
+ public function belongsTo($name, $targetClass)
+ {
+ $relation = $this->create(BelongsTo::class, $name, $targetClass);
+
+ $this->add($relation);
+
+ return $relation;
+ }
+
+ /**
+ * Define a many-to-many relationship
+ *
+ * @param string $name Name of the relation
+ * @param string $targetClass Target model class
+ *
+ * @return BelongsToMany
+ */
+ public function belongsToMany($name, $targetClass)
+ {
+ $relation = $this->create(BelongsToMany::class, $name, $targetClass);
+
+ $this->add($relation);
+
+ return $relation;
+ }
+
+ public function getIterator(): Traversable
+ {
+ return new ArrayIterator($this->relations);
+ }
+
+ /**
+ * Throw exception if a relation with the given name already exists
+ *
+ * @param string $name
+ */
+ protected function assertRelationDoesNotYetExist($name)
+ {
+ if ($this->has($name)) {
+ throw new \InvalidArgumentException("Relation '$name' already exists");
+ }
+ }
+
+ /**
+ * Throw exception if a relation with the given name does not exist
+ *
+ * @param string $name
+ */
+ protected function assertRelationExists($name)
+ {
+ if (! $this->has($name)) {
+ throw new InvalidRelationException($name);
+ }
+ }
+}