Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
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
You can’t perform that action at this time.