Skip to content

Commit

Permalink
save WIP: namespace, better readme, better class names
Browse files Browse the repository at this point in the history
  • Loading branch information
PhilippGrashoff committed Sep 6, 2023
1 parent 5b84186 commit 40dfd53
Show file tree
Hide file tree
Showing 13 changed files with 175 additions and 143 deletions.
94 changes: 47 additions & 47 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,56 +1,56 @@
# mtomforatk
[![codecov](https://codecov.io/gh/PhilippGrashoff/mtomforatk/branch/master/graph/badge.svg)](https://codecov.io/gh/PhilippGrashoff/mtomforatk)

An addition to atk4/data to easily manage Many To Many (MToM) Relations. The purpose
is to write as little code as possible for actual MToM operations.

## Example code
As Example, lets use Students and Lessons. A Teacher can have many Lessons, a Lesson can have many Teachers.
To map this MToM relationship, 3 classes are created:
* Teacher
* Lesson
* TeacherToLesson

After setting these classes up using this project, MToM operations can be done easily:
```
$teacher = (new Teacher($persistence))->createEntity();
$teacher->save();
//Add Lesson by its ID, in this case 123
$teacherToLesson = $teacher->addMToMRelation(new TeacherToLesson($persistence), 123); //creates a new TeacherToLesson record
$lessonWithId123 = $teacherToLesson->getReferencedEntity(Lesson::class); //easy way to get Lesson object. No extra DB query is used.
//remove lesson by its ID
$teacher->removeMToMRelation(new TeacherToLesson($persistence), 123); //removes the TeacherToLesson record
//Add a lesson by passing the Entity
$lesson = (new Lesson($persistence))->createEntity();
$lesson->save();
$teacher->addMToMRelation(new TeacherToLesson($persistence), $lesson);
$teacher->hasMToMRelation(new TeacherToLesson($persistence), $lesson); //true
//remove a lesson by passing object
$teacher->removeMToMRelation(new TeacherToLesson($persistence), $lesson);
$teacher->hasMToMRelation(new TeacherToLesson($persistence), $lesson); //falses
```

If you want even more comfort, implement some wrapper functions which further shorten the code.
As Example, another MToMRelation is set up in test/testmodels: StudentToLesson. A Student can
have many Lessons, a Lesson can have many Students:
* Student
* StudentToLesson

See Student where addLesson(), removeLesson() and hasLessonRelation() wrapper functions are implemented:
```
$student->addLesson($lesson);
$student->hasLessonRelation($lesson); //true
$student->removeLession($lesson);
$student->hasLessonRelation($lesson); //false
```

## Project Content
# Project Content
The project consists of two files:
* MToMModel: A base model for the intermediate class (like StudentToLesson). Working descendants can be coded with a few lines of code.
* MToMRelationForModelTrait: A Trait which is added to the models to be linked, (like Student and Lesson). With this trait, only a few more lines need to be added to make operations like `$lesson->addStudent(5);` work.

For an example implementation, have a look at tests/testmodels. Here you can find Student, Lesson and StudentToLesson Models.
# How to use
## Installation
The easiest way to use this repository is to add it to your composer.json in the require section:
```json
{
"require": {
"philippgrashoff/cronforatk": "4.0.*"
}
}
```
## Sample code
As example, lets use Students and Lessons. A Student can have many Lessons, a Lesson can have many Students.
To map this MToM relationship, 3 classes are created. Demo models for this example can be found in tests\Testmodels:
* Student: A normal model which additionally uses ModelWithMToMTrait. 3 little helpers methods are implemented to make MToM handling easier: addLesson(), removeLesson() and hasLesson();
* Lesson: A normal model which additionally uses ModelWithMToMTrait. 3 little helpers methods are implemented to make MToM handling easier: addStudent(), removeStudent() and hasStudent();
* StudentToLesson: The intermediate model carrying the student_id and lesson_id for each MToM relation between Students and Lessons.

After setting these classes up using this project, MToM operations can be done easily:
```php
$studentHarry = (new Student($persistence))->createEntity();
$studentHarry->set('name', 'Harry');
$studentHarry->save();
$lessonGeography = (new Lesson($persistence))->createEntity();
$lessonGeography->set('name', 'Geography');
$lessonGeography->save();

//now, lets easily add Harry to the Geography lesson:
$studentHarry->addLesson($lessonGeography);
//the above line created a StudentToLesson record with student_id = studentHarry's ID and lesson_id = lessonGeography's ID

//let's add Harry to another lesson
$lessonBiology = (new Lesson($persistence))->createEntity();
$lessonBiology->set('name', 'Biology');
$lessonBiology->save();
//adding/removing can either be done by passong the other model or only it's ID. In this case, we just pass the ID
$studentHarry->addLesson($lessonBiology->getId());
//this created another StudentToLesson record with student_id = studentHarry's ID and lesson_id = lessonBiology's ID

//Let's easily check if an MToM relation exists
$studentHarry->hasLesson($lessonGeography); //true;

//harry is tired of Geography, lets remove him from this lesson:
$studentHarry->removeLesson($lessonGeography);
//this removed the StudentToLesson Record linking Harry to Geography.
$studentHarry->hasLesson($lessonGeography); //false
```
4 changes: 2 additions & 2 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,12 @@
},
"autoload": {
"psr-4": {
"mtomforatk\\": "src"
"PhilippR\\Atk4\\MToM\\": "src"
}
},
"autoload-dev": {
"psr-4": {
"mtomforatk\\tests\\": "tests"
"PhilippR\\Atk4\\MToM\\Tests\\": "tests"
}
}
}
33 changes: 33 additions & 0 deletions docs/sample.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
<?php declare(strict_types=1);

use PhilippR\Atk4\MToM\Tests\Testmodels\Lesson;
use PhilippR\Atk4\MToM\Tests\Testmodels\Student;

$persistence = new \Atk4\Data\Persistence\Sql('sqlite::memory:');

$studentHarry = (new Student($persistence))->createEntity();
$studentHarry->set('name', 'Harry');
$studentHarry->save();
$lessonGeography = (new Lesson($persistence))->createEntity();
$lessonGeography->set('name', 'Geography');
$lessonGeography->save();

//now, lets easily add Harry to the Geography lesson:
$studentHarry->addLesson($lessonGeography);
//the above line created a StudentToLesson record with student_id = studentHarry's ID and lesson_id = lessonGeography's ID

//let's add Harry to another lesson
$lessonBiology = (new Lesson($persistence))->createEntity();
$lessonBiology->set('name', 'Biology');
$lessonBiology->save();
//adding/removing can either be done by passong the other model or only it's ID. In this case, we just pass the ID
$studentHarry->addLesson($lessonBiology->getId());
//this created another StudentToLesson record with student_id = studentHarry's ID and lesson_id = lessonBiology's ID

//Let's easily check if an MToM relation exists
$studentHarry->hasLesson($lessonGeography); //true;

//harry is tired of Geography, lets remove him from this lesson:
$studentHarry->removeLesson($lessonGeography);
//this removed the StudentToLesson Record linking Harry to Geography.
$studentHarry->hasLesson($lessonGeography); //false
32 changes: 16 additions & 16 deletions src/MToMModel.php → src/IntermediateModel.php
Original file line number Diff line number Diff line change
@@ -1,24 +1,24 @@
<?php declare(strict_types=1);

namespace mtomforatk;
namespace PhilippR\Atk4\MToM;

use Atk4\Data\Exception;
use Atk4\Data\Model;


abstract class MToMModel extends Model
abstract class IntermediateModel extends Model
{

/**
* @var array<string,class-string|null> $fieldNamesForReferencedEntities
* @var array<string,class-string|null> $relationFieldNames
* with 2 keys and 2 values. Set these four strings child classes.
* Will be used to create hasOne Reference Fields in init()
* e.g. [
* 'student_id' => Student::class,
* 'lesson_id' => Lesson::class
* ]
*/
protected array $fieldNamesForReferencedEntities = [];
protected array $relationFieldNames = [];

/**
* @var array<class-string,Model|null> $referencedEntities
Expand All @@ -38,19 +38,19 @@ protected function init(): void
{
parent::init();
//make sure 2 classes to link are defined
if (count($this->fieldNamesForReferencedEntities) !== 2) {
if (count($this->relationFieldNames) !== 2) {
throw new Exception(
'2 Fields and corresponding classes need to be defined in fieldNamesForReferencedClasses array'
);
}
if (
!class_exists(reset($this->fieldNamesForReferencedEntities))
|| !class_exists(end($this->fieldNamesForReferencedEntities))
!class_exists(reset($this->relationFieldNames))
|| !class_exists(end($this->relationFieldNames))
) {
throw new Exception('Non existent Class defined in fieldNamesForReferencedClasses array');
throw new Exception('Non existent Class defined in $relationFieldNames array');
}

foreach ($this->fieldNamesForReferencedEntities as $fieldName => $className) {
foreach ($this->relationFieldNames as $fieldName => $className) {
/** @var class-string $className */
$this->hasOne($fieldName, ['model' => [$className], 'required' => true]);
$this->referencedEntities[$className] = null;
Expand All @@ -64,7 +64,7 @@ protected function init(): void
* $lesson = $studentToLesson->getReferenceEntity(Lesson::class); //will return Lesson record with ID 4
*
* @param class-string<Model> $className
* @return Model|null
* @return Model
* @throws Exception
*/
public function getReferencedEntity(string $className): Model
Expand All @@ -79,7 +79,7 @@ public function getReferencedEntity(string $className): Model
$model = new $className($this->getPersistence());
//will throw exception if record isn't found
$this->referencedEntities[$className] = $model->load(
$this->get(array_search($className, $this->fieldNamesForReferencedEntities))
$this->get(array_search($className, $this->relationFieldNames))
);
}

Expand Down Expand Up @@ -116,7 +116,7 @@ public function addReferencedEntity(Model $entity): void
*/
public function getFieldNameForModel(Model $model): string
{
$fieldName = array_search(get_class($model), $this->fieldNamesForReferencedEntities);
$fieldName = array_search(get_class($model), $this->relationFieldNames);
if (!$fieldName) {
throw new Exception(
'No field name defined in $fieldNamesForReferencedEntities for Class ' . get_class($model)
Expand Down Expand Up @@ -152,14 +152,14 @@ public function addConditionForModel(Model $entity): void
public function getOtherModelClass(Model $model): string
{
$modelClass = get_class($model);
if (!in_array($modelClass, $this->fieldNamesForReferencedEntities)) {
if (!in_array($modelClass, $this->relationFieldNames)) {
throw new Exception('Class ' . $modelClass . 'not found in fieldNamesForReferencedClasses');
}

//as array has 2 elements, return second if passed class is the first, else otherwise
if (reset($this->fieldNamesForReferencedEntities) === $modelClass) {
return end($this->fieldNamesForReferencedEntities);
if (reset($this->relationFieldNames) === $modelClass) {
return end($this->relationFieldNames);
}
return reset($this->fieldNamesForReferencedEntities);
return reset($this->relationFieldNames);
}
}
54 changes: 27 additions & 27 deletions src/ModelWithMToMTrait.php → src/MToMTait.php
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<?php declare(strict_types=1);

namespace mtomforatk;
namespace PhilippR\Atk4\MToM;

use Atk4\Data\Exception;
use Atk4\Data\Model;
Expand All @@ -10,37 +10,37 @@
/**
* @extends Model<Model>
*/
trait ModelWithMToMTrait
trait MToMTait
{

/**
* Create a new MToM relation, e.g. a new StudentToLesson record. Called from either Student or Lesson class.
* First checks if record does exist already, and only then adds new relation.
*
* @param MToMModel $mToMModel
* @param string|int|Model $otherModel //if string or int, then it's only an ID
* @param IntermediateModel $mToMModel
* @param int|Model $otherEntity //if int, then it's only an ID
* @param array<string,mixed> $additionalFields
* @return MToMModel
* @return IntermediateModel
* @throws Exception
* @throws \Atk4\Core\Exception
*/
public function addMToMRelation(
MToMModel $mToMModel,
string|int|Model $otherModel,
IntermediateModel $mToMModel,
int|Model $otherEntity,
array $additionalFields = []
): MToMModel {
): IntermediateModel {
//$this needs to be loaded to get ID
$this->assertIsLoaded();
$otherModel = $this->getOtherEntity($otherModel, $mToMModel);
$otherEntity = $this->getOtherEntity($otherEntity, $mToMModel);
//check if reference already exists, if so update existing record only
$mToMModel->addConditionForModel($this);
$mToMModel->addConditionForModel($otherModel);
$mToMModel->addConditionForModel($otherEntity);
//no reload necessary after insert
$mToMModel->reloadAfterSave = false;
$mToMEntity = $mToMModel->tryLoadAny() ?? $mToMModel->createEntity();

$mToMEntity->set($mToMEntity->getFieldNameForModel($this), $this->getId());
$mToMEntity->set($mToMEntity->getFieldNameForModel($otherModel), $otherModel->getId());
$mToMEntity->set($mToMEntity->getFieldNameForModel($otherEntity), $otherEntity->getId());

//set additional field values
foreach ($additionalFields as $fieldName => $value) {
Expand All @@ -50,7 +50,7 @@ public function addMToMRelation(
//if that record already exists mysql will throw an error if unique index is set, catch here
$mToMEntity->save();
$mToMEntity->addReferencedEntity($this);
$mToMEntity->addReferencedEntity($otherModel);
$mToMEntity->addReferencedEntity($otherEntity);

return $mToMEntity;
}
Expand All @@ -60,19 +60,19 @@ public function addMToMRelation(
* method used to remove a MToMModel record like StudentToLesson. Either used from Student or Lesson class.
* GuestToGroup etc.
*
* @param MToMModel $mToMModel
* @param string|int|Model $otherModel //if string or int, then it's only an ID
* @return MToMModel
* @param IntermediateModel $mToMModel
* @param int|Model $otherEntity //if int, then it's only an ID
* @return IntermediateModel
* @throws Exception
*/
public function removeMToMRelation(MToMModel $mToMModel, string|int|Model $otherModel): MToMModel
public function removeMToMRelation(IntermediateModel $mToMModel, int|Model $otherEntity): IntermediateModel
{
//$this needs to be loaded to get ID
$this->assertIsLoaded();
$otherModel = $this->getOtherEntity($otherModel, $mToMModel);
$otherEntity = $this->getOtherEntity($otherEntity, $mToMModel);

$mToMModel->addConditionForModel($this);
$mToMModel->addConditionForModel($otherModel);
$mToMModel->addConditionForModel($otherEntity);
//loadAny as it will throw exception when record is not found
$mToMModel = $mToMModel->loadAny();
$mToMModel->delete();
Expand All @@ -85,18 +85,18 @@ public function removeMToMRelation(MToMModel $mToMModel, string|int|Model $other
* checks if a MtoM reference to the given entity exists or not, e.g. if a StudentToLesson record exists for a
* specific student and lesson
*
* @param MToMModel $mToMModel
* @param string|int|Model $otherModel //if string or int, then it's only an ID
* @param IntermediateModel $mToMModel
* @param int|Model $otherEntity //if int, then it's only an ID
* @return bool
* @throws Exception
*/
public function hasMToMRelation(MToMModel $mToMModel, string|int|Model $otherModel): bool
public function hasMToMRelation(IntermediateModel $mToMModel, int|Model $otherEntity): bool
{
$this->assertIsLoaded();
$otherModel = $this->getOtherEntity($otherModel, $mToMModel);
$otherEntity = $this->getOtherEntity($otherEntity, $mToMModel);

$mToMModel->addConditionForModel($this);
$mToMModel->addConditionForModel($otherModel);
$mToMModel->addConditionForModel($otherEntity);
$mToMEntity = $mToMModel->tryLoadAny();

return $mToMEntity !== null;
Expand All @@ -108,7 +108,7 @@ public function hasMToMRelation(MToMModel $mToMModel, string|int|Model $otherMod
* This way, no outdated intermediate models exist.
* Returns HasMany reference for further modifying reference if needed.
*
* @param class-string<MToMModel> $mtomClassName
* @param class-string<IntermediateModel> $mtomClassName
* @param string $referenceName
* @param array<string,mixed> $referenceDefaults
* @param array<string,mixed> $mtomClassDefaults
Expand Down Expand Up @@ -156,12 +156,12 @@ function ($model) use ($referenceName): void {
* Make sure passed model is of the correct class.
* Check other model is loaded so id can be gotten.
*
* @param string|int|Model $otherEntity //if string or int, then it's only an ID
* @param MToMModel $mToMModel
* @param int|Model $otherEntity //if int, then it's only an ID
* @param IntermediateModel $mToMModel
* @return Model
* @throws Exception
*/
protected function getOtherEntity(string|int|Model $otherEntity, MToMModel $mToMModel): Model
protected function getOtherEntity(int|Model $otherEntity, IntermediateModel $mToMModel): Model
{
$otherModelClass = $mToMModel->getOtherModelClass($this);
if (is_object($otherEntity)) {
Expand Down
Loading

0 comments on commit 40dfd53

Please sign in to comment.