Skip to content

Commit

Permalink
Model relation use class rather than id
Browse files Browse the repository at this point in the history
  • Loading branch information
olvlvl committed Sep 9, 2023
1 parent 2556c86 commit c22423e
Show file tree
Hide file tree
Showing 12 changed files with 80 additions and 67 deletions.
7 changes: 4 additions & 3 deletions lib/ActiveRecord/Config/BelongsToAssociation.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

namespace ICanBoogie\ActiveRecord\Config;

use ICanBoogie\ActiveRecord\Model;

/**
* A _belong to_ association between two models.
*
Expand All @@ -11,7 +13,7 @@ final class BelongsToAssociation
{
/**
* @param array{
* associate: string,
* associate: class-string<Model>,
* local_key: string,
* foreign_key: string,
* as: string,
Expand All @@ -23,8 +25,7 @@ public static function __set_state(array $an_array): self
}

/**
* @param string $associate
* A model identifier.
* @param class-string<Model> $associate
*/
public function __construct(
public readonly string $associate,
Expand Down
9 changes: 7 additions & 2 deletions lib/ActiveRecord/Config/HasManyAssociation.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

namespace ICanBoogie\ActiveRecord\Config;

use ICanBoogie\ActiveRecord\Model;

/**
* An _has many_ association between two models.
*
Expand All @@ -11,7 +13,7 @@ final class HasManyAssociation
{
/**
* @param array{
* model_id: string,
* associate: class-string<Model>,
* local_key: string,
* foreign_key: string,
* as: string,
Expand All @@ -23,8 +25,11 @@ public static function __set_state(array $an_array): self
return new self(...$an_array);
}

/**
* @param class-string<Model> $associate
*/
public function __construct(
public readonly string $model_id,
public readonly string $associate,
public readonly string $local_key,
public readonly string $foreign_key,
public readonly string $as,
Expand Down
12 changes: 5 additions & 7 deletions lib/ActiveRecord/ConfigBuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -223,7 +223,7 @@ private function resolve_belongs_to(
?? singularize($associate->id);

return new BelongsToAssociation(
$associate->id,
$associate->model_class,
$local_key,
$foreign_key,
$as,
Expand Down Expand Up @@ -268,11 +268,11 @@ private function resolve_has_many(
}

return new HasManyAssociation(
$related->id,
$related->model_class,
$local_key,
$foreign_key,
$as,
$through?->id,
$through?->model_class,
);
}

Expand Down Expand Up @@ -435,8 +435,7 @@ public function use_attributes(): self
/**
* Creates a schema builder and an association builder, if attributes are enabled they are configured using them.
*
* @param class-string $activerecord_class
* An ActiveRecord class.
* @param class-string<ActiveRecord> $activerecord_class
*
* @return array{ SchemaBuilder, AssociationBuilder }
*/
Expand All @@ -459,8 +458,7 @@ private function create_builders(string $activerecord_class): array
}

/**
* @param class-string $activerecord_class
* An ActiveRecord class.
* @param class-string<ActiveRecord> $activerecord_class
*
* @return array{
* TargetClass<SchemaAttribute>[],
Expand Down
25 changes: 17 additions & 8 deletions lib/ActiveRecord/HasManyRelation.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
namespace ICanBoogie\ActiveRecord;

use ICanBoogie\ActiveRecord;
use LogicException;
use PDO;

use function ICanBoogie\pluralize;
Expand All @@ -24,7 +25,8 @@ class HasManyRelation extends Relation
/**
* @inheritdoc
*
* @param string|null $through If the association is made indirectly, this is the name of that model.
* @param class-string<Model>|null $through
* A Model used as pivot.
*/
public function __construct(
Model $owner,
Expand All @@ -34,6 +36,10 @@ public function __construct(
string $as,
public readonly ?string $through = null,
) {
if ($through) {
ActiveRecord\Config\Assert::extends_model($through);
}

parent::__construct(
owner: $owner,
related: $related,
Expand All @@ -60,31 +66,34 @@ public function __invoke(ActiveRecord $record): Query
}

/**
* @param class-string<Model> $through
*
* @return Query<ActiveRecord>
*
* https://guides.rubyonrails.org/association_basics.html#the-has-many-through-association
*/
private function build_through_query(ActiveRecord $record, string $through_id): Query
private function build_through_query(ActiveRecord $record, string $through): Query
{
// $owner == $r1_model
// $related === $r2_model

$owner = $this->owner;
$related = $this->resolve_related();
$through = $this->ensure_model($through_id);
$r = $through->relations;
$r1 = $r->find(fn(Relation $r) => $r->related === $this->owner->id);
$r2 = $r->find(fn(Relation $r) => $r->related === $related->id);
$through_model = $this->ensure_model($through);
$r = $through_model->relations;
$r1 = $r->find(fn(Relation $r) => $r->related === $this->owner::class);
$r2 = $r->find(fn(Relation $r) => $r->related === $related::class)
?? throw new LogicException("Unable to find related model for " . $related::class);
$r2_model = $this->ensure_model($r2->related);

$q = $related->select("`{alias}`.*");
// Because of the select, we need to set the mode otherwise an array would be
// fetched instead of an object.
$q->mode(PDO::FETCH_CLASS, $related->activerecord_class);
//phpcs:disable Generic.Files.LineLength.TooLong
$q->join(expression: "INNER JOIN `$through->name` ON `$through->name`.{$r2->local_key} = `$r2_model->alias`.{$related->primary}");
$q->join(expression: "INNER JOIN `$through_model->name` ON `$through_model->name`.{$r2->local_key} = `$r2_model->alias`.{$related->primary}");
//phpcs:disable Generic.Files.LineLength.TooLong
$q->join(expression: "INNER JOIN `$owner->name` `$owner->alias` ON `$through->name`.{$r1->local_key} = `$owner->alias`.{$owner->primary}");
$q->join(expression: "INNER JOIN `$owner->name` `$owner->alias` ON `$through_model->name`.{$r1->local_key} = `$owner->alias`.{$owner->primary}");
$q->where("`$owner->alias`.{$owner->primary} = ?", $record->{$this->local_key});

return $q;
Expand Down
47 changes: 19 additions & 28 deletions lib/ActiveRecord/Model.php
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,7 @@ public function __construct(

parent::__construct($connection, $definition->table, $parent);

$this->resolve_relations();
$this->apply_associations($definition->association);
}

private function resolve_parent(ModelProvider $models): ?Model
Expand Down Expand Up @@ -165,47 +165,37 @@ private function resolve_parent(ModelProvider $models): ?Model
/**
* Resolves relations with other models.
*/
private function resolve_relations(): void
private function apply_associations(?ActiveRecord\Config\Association $association): void
{
$association = $this->definition->association;

if (!$association) {
return;
}

# belongs_to

$belongs_to = $association->belongs_to;

if ($belongs_to) {
foreach ($belongs_to as $r) {
$this->belongs_to(
related: $r->associate,
local_key: $r->local_key,
foreign_key: $r->foreign_key,
as: $r->as,
);
}
foreach ($association->belongs_to as $r) {
$this->belongs_to(
related: $r->associate,
local_key: $r->local_key,
foreign_key: $r->foreign_key,
as: $r->as,
);
}

# has_many

$has_many = $association->has_many;

if ($has_many) {
foreach ($has_many as $r) {
$this->has_many(
related: $r->model_id,
foreign_key: $r->foreign_key,
as: $r->as,
through: $r->through,
);
}
foreach ($association->has_many as $r) {
$this->has_many(
related: $r->associate,
foreign_key: $r->foreign_key,
as: $r->as,
through: $r->through,
);
}
}

/**
* @param string $related A module identifier.
* @param class-string<Model> $related
*/
public function belongs_to(
string $related,
Expand All @@ -224,7 +214,8 @@ public function belongs_to(
}

/**
* @param string $related A module identifier.
* @param class-string<Model> $related
* @param class-string<Model>|null $through
*/
public function has_many(
string $related,
Expand Down
19 changes: 12 additions & 7 deletions lib/ActiveRecord/Relation.php
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,10 @@
abstract class Relation
{
/**
* @param Model<int|string|string[], ActiveRecord> $owner
* @param Model $owner
* The parent model of the relation.
* @param string $related
* The related model of the relation. Can be specified using its instance or its identifier.
* @param class-string<Model> $related
* The class of the related Model.
* @param string $local_key
* The name of the column on the owner model.
* @param string $foreign_key
Expand All @@ -39,6 +39,8 @@ public function __construct(
public readonly string $foreign_key,
public readonly string $as,
) {
ActiveRecord\Config\Assert::extends_model($related);

$activerecord_class = $this->resolve_activerecord_class($owner);
$prototype = Prototype::from($activerecord_class);

Expand Down Expand Up @@ -100,12 +102,15 @@ protected function resolve_related(): Model
return $this->ensure_model($this->related);
}

protected function ensure_model(Model|string $model_or_id): Model
/**
* @param Model|class-string<Model> $model_or_class
*/
protected function ensure_model(Model|string $model_or_class): Model
{
if ($model_or_id instanceof Model) {
return $model_or_id;
if ($model_or_class instanceof Model) {
return $model_or_class;
}

return $this->owner->models->model_for_id($model_or_id);
return $this->owner->models->model_for_class($model_or_class);
}
}
4 changes: 1 addition & 3 deletions lib/ActiveRecord/RelationCollection.php
Original file line number Diff line number Diff line change
Expand Up @@ -116,9 +116,7 @@ public function has_many(
}

/**
* @param (Closure(Relation, string $as): ?Relation) $predicate
*
* @return Relation|null
* @param (Closure(Relation, string $as): bool) $predicate
*/
public function find(Closure $predicate): ?Relation
{
Expand Down
1 change: 1 addition & 0 deletions phpstan.neon
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,4 @@ parameters:
- "#ActiveRecord but does not specify its types#"
- "#with generic class ICanBoogie\\\\ActiveRecord does not specify its types#"
- "#with generic class .+Model does not specify its types#"
- "#with generic class .+Model but does not specify its types#"
8 changes: 4 additions & 4 deletions tests/ActiveRecord/ConfigBuilderTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -102,12 +102,12 @@ public function test_from_attributes_with_association(): void

$this->assertNotNull($ap_def->association);
$this->assertEquals([
new Config\BelongsToAssociation('physicians', 'physician_id', 'ph_id', 'physician'),
new Config\BelongsToAssociation('patients', 'patient_id', 'pa_id', 'patient'),
new Config\BelongsToAssociation(PhysicianModel::class, 'physician_id', 'ph_id', 'physician'),
new Config\BelongsToAssociation(PatientModel::class, 'patient_id', 'pa_id', 'patient'),
], $ap_def->association->belongs_to);
$this->assertEquals([
new Config\HasManyAssociation('appointments', 'ph_id', 'physician_id', 'appointments', null),
new Config\HasManyAssociation('patients', 'ph_id', 'pa_id', 'patients', 'appointments'),
new Config\HasManyAssociation(AppointmentModel::class, 'ph_id', 'physician_id', 'appointments', null),
new Config\HasManyAssociation(PatientModel::class, 'ph_id', 'pa_id', 'patients', AppointmentModel::class),
], $ph_def->association->has_many);
}

Expand Down
6 changes: 4 additions & 2 deletions tests/ActiveRecord/HasManyRelationTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@
use ICanBoogie\ActiveRecord\RelationCollection;
use ICanBoogie\ActiveRecord\RelationNotDefined;
use PHPUnit\Framework\TestCase;
use Test\ICanBoogie\Acme\Comment;
use Test\ICanBoogie\Acme\CommentModel;
use Test\ICanBoogie\Fixtures;

final class HasManyRelationTest extends TestCase
Expand Down Expand Up @@ -61,7 +63,7 @@ public function test_relations(): void
$this->assertInstanceOf(HasManyRelation::class, $relation);
$this->assertSame('comments', $relation->as);
$this->assertSame($this->articles, $relation->owner);
$this->assertSame('comments', $relation->related);
$this->assertSame(CommentModel::class, $relation->related);
$this->assertSame('nid', $relation->local_key);
$this->assertSame('nid', $relation->foreign_key);
}
Expand Down Expand Up @@ -96,7 +98,7 @@ public function test_getter_as(): void
{
$articles = $this->articles;
$articles->has_many(
related: 'comments',
related: CommentModel::class,
foreign_key: 'nid',
as: 'article_comments'
);
Expand Down
3 changes: 2 additions & 1 deletion tests/ActiveRecord/HasManyRelationThroughTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
use ICanBoogie\ActiveRecord\Query;
use PHPUnit\Framework\TestCase;
use Test\ICanBoogie\Acme\HasMany\Appointment;
use Test\ICanBoogie\Acme\HasMany\AppointmentModel;
use Test\ICanBoogie\Acme\HasMany\Patient;
use Test\ICanBoogie\Acme\HasMany\Physician;
use Test\ICanBoogie\Fixtures;
Expand Down Expand Up @@ -74,7 +75,7 @@ public function test_through_is_set(): void
$this->assertEquals('patients', $rp->as);
$this->assertEquals('ph_id', $rp->local_key);
$this->assertEquals('pa_id', $rp->foreign_key);
$this->assertEquals('appointments', $rp->through);
$this->assertEquals(AppointmentModel::class, $rp->through);
}

public function test_physician_has_many_appointments(): void
Expand Down
Loading

0 comments on commit c22423e

Please sign in to comment.