Skip to content

Commit

Permalink
relation documentation and fixes.
Browse files Browse the repository at this point in the history
  • Loading branch information
n0nag0n committed Jan 19, 2024
1 parent 5c457e5 commit 8ede375
Show file tree
Hide file tree
Showing 5 changed files with 251 additions and 173 deletions.
34 changes: 29 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -295,23 +295,47 @@ $user->isNotNull('id')->find();
### Relationships
You can set several kinds of relationships using this library. You can set one->many and one->one relationships between tables. This requires a little extra setup in the class beforehand.

Setting the `$relations` array is not hard, but guessing the correct syntax can be confusing.

```php
public $relations = [
// you can name the key anything you'd like. The name of the ActiveRecord is probably good. Ex: user, contact, client
'whatever_active_record' => [
// required
self::HAS_ONE, // this is the type of relationship

// required
'Some_Class', // this is the "other" ActiveRecord class this will reference

// required
'local_key', // this is the local_key that references the join.
// just FYI, this also only joins to the primary key of the "other" model

// optional
[ 'eq' => 1, 'select' => 'COUNT(*) as count', 'limit' 5 ], // custom methods you want executed. [] if you don't want any.

// optional
'back_reference_name' // this is if you want to back reference this relationship back to itself Ex: $user->contact->user;
];
]
```

```php
class User extends ActiveRecord{
public $table = 'user';
public $primaryKey = 'id';
public $relations = [
'contacts' => [ self::HAS_MANY, 'Contact', 'user_id' ],
'contact' => [ self::HAS_ONE, 'Contact', 'user_id' ],
'contacts' => [ self::HAS_MANY, Contact::class, 'user_id' ],
'contact' => [ self::HAS_ONE, Contact::class, 'user_id' ],
];
}

class Contact extends ActiveRecord{
public $table = 'contact';
public $primaryKey = 'id';
public $relations = [
'user' => [ self::BELONGS_TO, 'User', 'user_id' ],
'user_with_backref' => [ self::BELONGS_TO, 'User', 'user_id', [], 'contact' ],
// using 5th param to define backref
'user' => [ self::BELONGS_TO, User::class, 'user_id' ],
'user_with_backref' => [ self::BELONGS_TO, User::class, 'user_id', [], 'contact' ],
];
}
```
Expand Down
213 changes: 118 additions & 95 deletions src/ActiveRecord.php
Original file line number Diff line number Diff line change
Expand Up @@ -127,30 +127,30 @@ abstract class ActiveRecord extends Base
'group' => null
];

/**
* Possible Events that can be run on the Active Record
*
* @var array
*/
protected array $events = [
'beforeInsert',
'afterInsert',
'beforeUpdate',
'afterUpdate',
'beforeSave',
'afterSave',
'beforeDelete',
'afterDelete',
'beforeFind',
'afterFind',
'beforeFindAll',
'afterFindAll'
];

/**
* @var array Stored the SQL Expressions of the SQL.
*/
protected array $expressions = [];
/**
* Possible Events that can be run on the Active Record
*
* @var array
*/
protected array $events = [
'beforeInsert',
'afterInsert',
'beforeUpdate',
'afterUpdate',
'beforeSave',
'afterSave',
'beforeDelete',
'afterDelete',
'beforeFind',
'afterFind',
'beforeFindAll',
'afterFindAll'
];

/**
* @var array Stored the SQL Expressions of the SQL.
*/
protected array $expressions = [];

/**
* @var array Stored the Expressions of the SQL.
Expand Down Expand Up @@ -236,7 +236,7 @@ public function __call($name, $args)
'operator' => $this->sqlParts[$name],
'target' => implode(', ', $args)
]);
}
}
return $this;
}

Expand Down Expand Up @@ -299,11 +299,17 @@ public function setCustomData(string $key, $value): void
$this->custom_data[$key] = $value;
}

public function clearData(): self
{
$this->data = [];
return $this;
}

/**
* function to reset the $params and $sqlExpressions.
* @return ActiveRecord return $this, can using chain method calls.
*/
protected function resetQueryData()
protected function resetQueryData(): self
{
$this->params = [];
$this->sqlExpressions = [];
Expand Down Expand Up @@ -350,11 +356,11 @@ public function find($id = null)
$this->resetQueryData()->eq($this->primaryKey, $id);
}

$this->processEvent('beforeFind', [ $this ]);
$this->processEvent('beforeFind', [ $this ]);

$result = $this->query($this->limit(1)->buildSql(['select', 'from', 'join', 'where', 'group', 'having', 'order', 'limit', 'offset']), $this->params, $this->resetQueryData(), true);
$result = $this->query($this->limit(1)->buildSql(['select', 'from', 'join', 'where', 'group', 'having', 'order', 'limit', 'offset']), $this->params, $this->resetQueryData(), true);

$this->processEvent('afterFind', [ $result ]);
$this->processEvent('afterFind', [ $result ]);

return $result;
}
Expand All @@ -365,20 +371,20 @@ public function find($id = null)
public function findAll(): array
{

$this->processEvent('beforeFindAll', [ $this ]);
$results = $this->query($this->buildSql(['select', 'from', 'join', 'where', 'group', 'having', 'order', 'limit', 'offset']), $this->params, $this->resetQueryData());
$this->processEvent('afterFindAll', [ $results ]);
return $results;
$this->processEvent('beforeFindAll', [ $this ]);
$results = $this->query($this->buildSql(['select', 'from', 'join', 'where', 'group', 'having', 'order', 'limit', 'offset']), $this->params, $this->resetQueryData());
$this->processEvent('afterFindAll', [ $results ]);
return $results;
}
/**
* function to delete current record in database.
* @return bool
*/
public function delete()
{
$this->processEvent('beforeDelete', [ $this ]);
$result = $this->execute($this->eq($this->primaryKey, $this->{$this->primaryKey})->buildSql(['delete', 'from', 'where']), $this->params);
$this->processEvent('afterDelete', [ $this ]);
$this->processEvent('beforeDelete', [ $this ]);
$result = $this->execute($this->eq($this->primaryKey, $this->{$this->primaryKey})->buildSql(['delete', 'from', 'where']), $this->params);
$this->processEvent('afterDelete', [ $this ]);
return $result;
}
/**
Expand All @@ -397,16 +403,16 @@ public function insert(): ActiveRecord
]);
$this->values = new Expressions(['operator'=> 'VALUES', 'target' => new WrapExpressions(['target' => $value])]);

$this->processEvent([ 'beforeInsert', 'beforeSave' ], [ $this ]);
$this->execute($this->buildSql(['insert', 'values']), $this->params);
$this->processEvent([ 'beforeInsert', 'beforeSave' ], [ $this ]);
$this->execute($this->buildSql(['insert', 'values']), $this->params);
$this->{$this->primaryKey} = $this->pdo->lastInsertId();

$this->processEvent([ 'afterInsert', 'afterSave' ], [ $this ]);
$this->processEvent([ 'afterInsert', 'afterSave' ], [ $this ]);

return $this->dirty()->resetQueryData();
return $this->dirty()->resetQueryData();
}
/**
/**
* function to build update SQL, and update current record in database, just write the dirty data into database.
* @return ActiveRecord if update success return current object
*/
Expand All @@ -419,28 +425,28 @@ public function update(): ActiveRecord
$this->addCondition($field, '=', $value, ',', 'set');
}

$this->processEvent([ 'beforeUpdate', 'beforeSave' ] , [ $this ]);
$this->processEvent([ 'beforeUpdate', 'beforeSave' ], [ $this ]);

$this->execute($this->eq($this->primaryKey, $this->{$this->primaryKey})->buildSql(['update', 'set', 'where']), $this->params);

$this->processEvent([ 'afterUpdate', 'afterSave' ] , [ $this ]);
$this->processEvent([ 'afterUpdate', 'afterSave' ], [ $this ]);

return $this->dirty()->resetQueryData();
}

/**
* Updates or inserts a record
*
* @return ActiveRecord
*/
public function save(): ActiveRecord
{
if($this->{$this->primaryKey}) {
return $this->update();
} else {
return $this->insert();
}
}
/**
* Updates or inserts a record
*
* @return ActiveRecord
*/
public function save(): ActiveRecord
{
if ($this->{$this->primaryKey}) {
return $this->update();
} else {
return $this->insert();
}
}
/**
* helper function to exec sql.
* @param string $sql The SQL need to be execute.
Expand All @@ -455,14 +461,14 @@ public function execute(string $sql, array $params = []): bool
throw new Exception($this->pdo->errorInfo()[2]);
}

$this->processEvent('beforeExecute', [ $statement, $params ]);
$this->processEvent('beforeExecute', [ $statement, $params ]);

$result = $statement->execute($params);
$result = $statement->execute($params);
if ($result === false) {
throw new Exception($statement->errorInfo()[2]);
}

$this->processEvent('afterExecute', [ $statement ]);
$this->processEvent('afterExecute', [ $statement ]);
return $result;
}
/**
Expand All @@ -477,7 +483,12 @@ public function query(string $sql, array $param = [], ActiveRecord $obj = null,
{
$sth = $this->pdo->prepare($sql);
$called_class = get_called_class();
$sth->setFetchMode(PDO::FETCH_INTO, ($obj ? $obj : new $called_class ));
$obj = $obj ?: new $called_class($this->pdo);

// Since we are finding a new record, this makes sure that nothing is persisted on the object since we're really looking for a new object.
$obj->clearData();

$sth->setFetchMode(PDO::FETCH_INTO, $obj);
$sth->execute($param);
if ($single) {
return $sth->fetch() ? $obj->dirty() : false;
Expand All @@ -491,33 +502,44 @@ public function query(string $sql, array $param = [], ActiveRecord $obj = null,
/**
* helper function to get relation of this object.
* There was three types of relations: {BELONGS_TO, HAS_ONE, HAS_MANY}
* @param string $name The name of the relation, the array key when defind the relation.
* @param string $name The name of the relation, the array key when defining the relation.
* @return mixed
*/
protected function &getRelation(string $name)
{
$relation = $this->relations[$name];
if ($relation instanceof self || (is_array($relation) && $relation[0] instanceof self)) {
if (is_array($relation) === true) {
// self::BELONGS_TO etc
$relation_type_or_object_name = $relation[0];
$relation_class = $relation[1] ?? '';
$relation_local_key = $relation[2] ?? '';
$relation_array_callbacks = $relation[3] ?? [];
$relation_back_reference = $relation[4] ?? '';
}

if ($relation instanceof self || $relation_type_or_object_name instanceof self) {
return $relation;
}
$this->relations[$name] = $obj = new $relation[1]($this->pdo);
if (isset($relation[3]) && is_array($relation[3])) {
foreach ((array)$relation[3] as $func => $args) {
call_user_func_array([$obj, $func], (array)$args);

$obj = new $relation_class($this->pdo);
$this->relations[$name] = $obj;
if ($relation_array_callbacks) {
foreach ($relation_array_callbacks as $method => $args) {
call_user_func_array([ $obj, $method ], (array) $args);
}
}
$backref = isset($relation[4]) ? $relation[4] : '';
if ((!$relation instanceof self) && self::HAS_ONE == $relation[0]) {
$obj->eq($relation[2], $this->{$this->primaryKey})->find() && $backref && $obj->__set($backref, $this);
} elseif (is_array($relation) && self::HAS_MANY == $relation[0]) {
$this->relations[$name] = $obj->eq($relation[2], $this->{$this->primaryKey})->findAll();
if ($backref) {

if ((!$relation instanceof self) && self::HAS_ONE === $relation_type_or_object_name) {
$obj->eq($relation_local_key, $this->{$this->primaryKey})->find() && $relation_back_reference && $obj->__set($relation_back_reference, $this);
} elseif (self::HAS_MANY === $relation_type_or_object_name) {
$this->relations[$name] = $obj->eq($relation_local_key, $this->{$this->primaryKey})->findAll();
if ($relation_back_reference) {
foreach ($this->relations[$name] as $o) {
$o->__set($backref, $this);
$o->__set($relation_back_reference, $this);
}
}
} elseif ((!$relation instanceof self) && self::BELONGS_TO == $relation[0]) {
$obj->eq($obj->primaryKey, $this->{$relation[2]})->find() && $backref && $obj->__set($backref, $this);
} elseif (!($relation instanceof self) && self::BELONGS_TO === $relation_type_or_object_name) {
$obj->eq($obj->primaryKey, $this->{$relation_local_key})->find() && $relation_back_reference && $obj->__set($relation_back_reference, $this);
}
return $this->relations[$name];
}
Expand Down Expand Up @@ -668,7 +690,7 @@ protected function addExpression(Expressions $expressions, string $delimiter)
$this->expressions[] = new Expressions(['operator' => $delimiter, 'target' => $expressions]);
}
}
/**
* helper function to add condition into WHERE.
* @param Expressions $exp The expression will be concat into WHERE or SET statement.
Expand All @@ -684,22 +706,23 @@ protected function addConditionGroup(Expressions $expressions, string $operator,
}
}

/**
* Process an event that's been set.
*
* @param string|array $event The name (or array of names) of the event from $this->events
* @param array $data_to_pass Usually ends up being $this
* @return void
*/
protected function processEvent($event, array $data_to_pass = []) {
if(is_array($event)=== false) {
$event = [ $event ];
}

foreach($event as $event_name) {
if(method_exists($this, $event_name) && in_array($event_name, $this->events, true) === true) {
$this->{$event_name}(...$data_to_pass);
}
}
}
/**
* Process an event that's been set.
*
* @param string|array $event The name (or array of names) of the event from $this->events
* @param array $data_to_pass Usually ends up being $this
* @return void
*/
protected function processEvent($event, array $data_to_pass = [])
{
if (is_array($event)=== false) {
$event = [ $event ];
}

foreach ($event as $event_name) {
if (method_exists($this, $event_name) && in_array($event_name, $this->events, true) === true) {
$this->{$event_name}(...$data_to_pass);
}
}
}
}
Loading

0 comments on commit 8ede375

Please sign in to comment.