Phalcon PHP modifiable array assignment of relationships on new objects
<?php | |
use Phalcon\Mvc\Model; | |
/** | |
* Configuration and behaviours for all models | |
*/ | |
abstract class BaseModel extends Model | |
{ | |
/** | |
* Marker if this model has been saved or came from the database | |
* (ID could be used instead, but this is more generic) | |
* @var bool | |
*/ | |
private $isNew = true; | |
/** | |
* List of relation aliases that have UnsavedRelationLists | |
* @var string[] | |
*/ | |
private $unsavedRelations = []; | |
/** | |
* Check if this instance has been written to or came from the database | |
* This could alternatively use Model::getDirtyState() | |
*/ | |
public function isNew() : bool { | |
return $this->isNew; | |
} | |
protected function afterSave() | |
{ | |
$this->isNew = false; | |
} | |
protected function afterFetch() | |
{ | |
$this->isNew = false; | |
} | |
/** | |
* Amend Phalcon's save routine to push related models into it's save queue | |
*/ | |
public function save($data=null, $whiteList=null) | |
{ | |
// Move any unsaved relations over to Phalcon to save | |
if (count($this->unsavedRelations)) { | |
$relations = $this->unsavedRelations; | |
$this->unsavedRelations = []; | |
foreach ($relations as $alias => $list) { | |
if (property_exists($this, $alias) && $this->$alias instanceof UnsavedRelationList) { | |
// Register related objects with Phalcon's relation tracking | |
if (!isset($this->_related)) { | |
$this->_related = []; | |
} | |
$this->_related[$alias] = $this->$alias->toArray(); | |
$this->_dirtyState = self::DIRTY_STATE_TRANSIENT; | |
} | |
} | |
} | |
return parent::save($data, $whiteList); | |
} | |
/** | |
* Make array-like relationships more consistent than default Phalcon behaviour | |
* | |
* @see Soulbudget\Model\Relations\UnsavedRelationList | |
*/ | |
public function __get($property) | |
{ | |
// Return an array-like object to work with relations before saving | |
// For non-new objects, ResultSet is returned. | |
if ($this->isNew()) { | |
// Check if this magic property is a relation | |
$relation = $this->getModelsManager()->getRelationByAlias(static::class, $property); | |
if ($relation && $relation->getType() == Model\Relation::HAS_MANY) { | |
$list = new UnsavedRelationList(); | |
// Set the member variable to this object so this getter is no longer called | |
$this->$property = $list; | |
// Remember in our own relation list | |
$this->unsavedRelations[strtolower($property)] = $list; | |
$this->setDirtyState(self::DIRTY_STATE_TRANSIENT); | |
return $list; | |
} | |
} | |
return parent::__get($property); | |
} | |
/** | |
* Replace array values being set on relations with UnsavedRelationLists for consistency | |
*/ | |
public function __set($property, $value) | |
{ | |
// Return an array-like object to work with relations before saving | |
// For non-new objects, ResultSet is returned. | |
if ($this->isNew() && is_array($value)) { | |
// Check if this magic property is a relation | |
$relation = $this->getModelsManager()->getRelationByAlias(static::class, $property); | |
if ($relation && ($relation->getType() == Model\Relation::HAS_MANY || $relation->getType() == Model\Relation::HAS_MANY_THROUGH)) { | |
$list = new UnsavedRelationList($value); | |
// Set the member variable to this list for later access | |
$this->$property = $list; | |
// Piggy-back on Phalcon's related list | |
$this->unsavedRelations[strtolower($property)] = $list; | |
return $value; | |
} | |
} | |
return parent::__set($property, $value); | |
} | |
} |
<?php | |
use Phalcon\Mvc\ModelInterface; | |
/** | |
* Array-like list of related models for unsaved items. | |
* | |
* Phalcon normally returns a ResultSet instance for unsaved relations, which isn't useful. | |
* This class aims to replace Phalcon's magic relation setter, which is limited to whole-array writes | |
* and cannot read back or appended to the relation array after setting. An UnsavedRelationList keeps | |
* implicit relations when saving new items, while making relations usable without writing and reading | |
* back from the database. | |
* | |
* Magic getters and setters can't work with arrays directly, so this object is passed around instead. | |
*/ | |
class UnsavedRelationList implements \ArrayAccess, \Iterator, \Countable | |
{ | |
/** | |
* List of relations | |
* @var ModelInterface[] | |
*/ | |
protected $members = []; | |
/** | |
* Cursor in the $members array | |
* @var int | |
*/ | |
protected $index = 0; | |
public function __construct($source = null) | |
{ | |
// Nothing to copy | |
if ($source === null) { | |
return; | |
} | |
// Copy members with type checking | |
if (is_array($source) || $source instanceof \Traversable) { | |
foreach ($source as $item) { | |
$this->offsetSet(null, $item); | |
} | |
} else { | |
// Unknown input | |
$type = $this->getTypeForError($source); | |
throw new \InvalidArgumentException("Source data must be an array or Traversable. Got $type"); | |
} | |
} | |
public function save() | |
{ | |
foreach ($this->members as $model) { | |
$model->save(); | |
} | |
} | |
public function toArray() : array | |
{ | |
return $this->members; | |
} | |
// Array-like interface implementations | |
/** | |
* @return ModelInterface | |
*/ | |
public function current() | |
{ | |
return $this->members[$this->index]; | |
} | |
public function next() | |
{ | |
$this->index++; | |
} | |
public function key() | |
{ | |
return $this->index; | |
} | |
public function valid() | |
{ | |
return $this->index < count($this->members); | |
} | |
public function rewind() | |
{ | |
$this->index = 0; | |
} | |
public function offsetExists($offset) | |
{ | |
return isset($this->members[$offset]); | |
} | |
/** | |
* @param mixed $offset | |
* @return ModelInterface | |
*/ | |
public function offsetGet($offset) | |
{ | |
return $this->members[$offset]; | |
} | |
public function offsetSet($offset, $value) | |
{ | |
// Only accept ModelInterfaces | |
if (!($value instanceof ModelInterface)) { | |
$type = $this->getTypeForError($value); | |
throw new \InvalidArgumentException("Value must be an instance of ModelInterface. Got $type"); | |
} | |
if ($offset === null) { | |
$this->members[] = $value; | |
} else { | |
$this->members[$offset] = $value; | |
} | |
} | |
public function offsetUnset($offset) | |
{ | |
unset($this->members[$offset]); | |
} | |
public function count() | |
{ | |
return count($this->members); | |
} | |
private function getTypeForError($value) : string | |
{ | |
if (is_object($value)) { | |
return get_class($value); | |
} else { | |
return gettype($value); | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment