Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support to create, update and delete using models #78

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
183 changes: 183 additions & 0 deletions src/Common/PropertiesState.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
<?php

namespace ipl\Orm\Common;

use ipl\Stdlib\Properties;

trait PropertiesState
{
use Properties {
Properties::setProperty as private parentSetProperty;
}

/**
* This model's modified properties
*
* @var array
*/
protected $modified = [];

/**
* Flag that indicate whether this is a new record
*
* @var bool
*/
protected $newRecord = false;

/**
* Flag whether this record has been already removed
*
* @var bool
*/
protected $removed = false;

/**
* Flag that indicate whether the primary key of this model is auto incremented
*
* @var bool
*/
protected $autoIncremented = true;

protected function setProperty($key, $value)
{
$this->parentSetProperty($key, $value);

return $this->setDirty($key);
}

/**
* Get whether this model has any modified properties
*
* @param array $properties
*
* @return bool
*/
protected function hasModified(...$properties)
{
if (empty($properties)) {
return ! empty($this->modified);
}

foreach ($properties as $key) {
if ($this->isModified($key)) {
return true;
}
}

return false;
}

/**
* Get all modified properties of this model
*
* @return array
*/
public function getModifiedProperties()
{
return array_intersect_key($this->properties, $this->modified);
}

/**
* Get the whether the given property is modified
*
* @param $key
*
* @return bool
*/
protected function isModified($key)
{
return isset($this->modified[$key]) && isset($this->properties[$key]);
}

/**
* Clear the modified properties of this model
*
* @return $this
*/
protected function resetDirty()
{
$this->modified = [];

return $this;
}

/**
* Get whether this model's state has remained unchanged
*
* @param mixed ...$properties
*
* @return bool
*/
public function isClean(...$properties): bool
{
return ! $this->isDirty(...$properties);
}

/**
* Get whether this model's state has been modified
*
* @param mixed ...$properties
*
* @return bool
*/
public function isDirty(...$properties): bool
{
return $this->hasModified(...$properties);
}

/**
* Mark the given property as dirty/modified
*
* @param $property
*
* @return $this
*/
public function setDirty($property)
{
$this->modified[$property] = true;

return $this;
}

/**
* Get whether this instance is a new record
*
* @return bool
*/
public function isNewRecord(): bool
{
return $this->newRecord;
}

/**
* @return bool
*/
public function isRemoved(): bool
{
return $this->removed;
}

/**
* Set whether this model's primary key is auto incremented
*
* @param bool $autoIncremented
*
* @return $this
*/
public function setAutoIncremented(bool $autoIncremented): self
{
$this->autoIncremented = $autoIncremented;

return $this;
}

/**
* Get whether the primary key of this model is auto incremented
*
* @return bool
*/
public function isAutoIncremented(): bool
{
return $this->autoIncremented;
}
}
4 changes: 2 additions & 2 deletions src/Common/PropertiesWithDefaults.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@

trait PropertiesWithDefaults
{
use \ipl\Stdlib\Properties {
\ipl\Stdlib\Properties::getProperty as private parentGetProperty;
use PropertiesState {
PropertiesState::getProperty as private parentGetProperty;
}

protected function getProperty($key)
Expand Down
168 changes: 167 additions & 1 deletion src/Model.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,12 @@
namespace ipl\Orm;

use ipl\Orm\Common\PropertiesWithDefaults;
use ipl\Orm\Compat\FilterProcessor;
use ipl\Sql\Connection;
use ipl\Sql\Delete;
use ipl\Sql\Insert;
use ipl\Sql\Update;
use ipl\Stdlib\Filter;

/**
* Models represent single database tables or parts of it.
Expand All @@ -13,12 +18,25 @@ abstract class Model implements \ArrayAccess, \IteratorAggregate
{
use PropertiesWithDefaults;

final public function __construct(array $properties = null)
/** @var string Indicates whether insert() has successfully inserted a new entry into the DB */
public const STATE_INSERTED = 'stateInserted';

/** @var string Whether the update() has updated this model successfully */
public const STATE_UPDATED = 'stateUpdated';

/** @var string Whether remove() has removed this model successfully */
public const STATE_REMOVED = 'stateRemoved';

/** @var string Whether this model is in clean state/is unchanged */
public const STATE_CLEAN = 'stateClean';

final public function __construct(array $properties = null, bool $isNewRecord = true)
{
if ($this->hasProperties()) {
$this->setProperties($properties);
}

$this->newRecord = $isNewRecord;
$this->init();
}

Expand Down Expand Up @@ -74,6 +92,21 @@ public static function on(Connection $db)
->setModel(new static());
}

/**
* Get the prepared insert query of this model
*
* @param Connection $conn
* @param array $properties
*
* @return ScopedQuery
*/
public static function insert(Connection $conn, array $properties)
{
return (new static())
->setProperties($properties)
->prepareInsert($conn);
}

/**
* Get the model's default sort
*
Expand Down Expand Up @@ -129,4 +162,137 @@ public function createRelations(Relations $relations)
protected function init()
{
}

/**
* Save this model to the database
*
* Determines automagically whether it INSERT or UPDATE this model
*
* @param Connection $conn
*
* @return string
*/
public function save(Connection $conn)
{
if ($this->isClean()) {
return self::STATE_CLEAN;
}

if (! $this->isNewRecord()) { // Is modified
$this->prepareUpdate($conn)->execute();

$this->resetDirty();

return self::STATE_UPDATED;
} else {
$this->prepareInsert($conn)->execute();

if ($this->isAutoIncremented()) {
$this->{$this->getKeyName()} = $conn->lastInsertId();
}

$this->newRecord = false;
$this->resetDirty();

return self::STATE_INSERTED;
}
}

/**
* Remove a database entry matching the given filter
*
* @param Connection $conn
* @param ?Filter\Rule $filter
*
* @return string
*/
public function remove(Connection $conn, Filter\Rule $filter = null)
{
if ($this->isNewRecord()) {
throw new \LogicException('Cannot delete an entry which does not exists');
}

if ($this->isRemoved()) {
throw new \LogicException('Cannot delete already deleted entry');
}

$delete = new Delete();
$delete->from($this->getTableName());
$this->applyFilter($delete, $filter);

$query = new ScopedQuery($conn, $delete);
$query->execute();

$this->removed = true;

return self::STATE_REMOVED;
}

protected function prepareUpdate(Connection $conn, Filter\Rule $filter = null)
{
if ($this->isNewRecord()) {
throw new \LogicException('Cannot update a new entry');
}

if ($this->isRemoved()) {
throw new \LogicException('Cannot update removed entry');
}

$update = new Update();
$update
->table($this->getTableName())
->set($this->getModifiedProperties());

$this->applyFilter($update, $filter);

return new ScopedQuery($conn, $update);
}

protected function prepareInsert(Connection $conn)
{
if (! $this->isNewRecord()) {
throw new \LogicException('Cannot insert already existing entry');
}

$properties = $this->getModifiedProperties();
if (empty($properties)) {
$properties = $this->properties;
}

if (! $this->isAutoIncremented() && ! isset($properties[$this->getKeyName()])) {
throw new \Exception('Cannot insert entry without a primary key');
}

$insert = new Insert();
$insert->into($this->getTableName());

$insert->values($properties);

return new ScopedQuery($conn, $insert);
}

/**
* Apply the given filter or create custom filter based on the PK(s)
*
* @param Insert|Update|Delete $query
* @param Filter\Rule|null $filter
*
* @return $this
*/
protected function applyFilter($query, Filter\Rule $filter = null)
{
if (! $filter) {
$filter = Filter::all();
$keys = ! is_array($this->getKeyName()) ? [$this->getKeyName()] : $this->getKeyName();

foreach ($keys as $key) {
$filter->add(Filter::equal($key, (int) $this->{$key}));
}
}

$where = FilterProcessor::assembleFilter($filter);
$query->where(...array_reverse($where));

return $this;
}
}
Loading