Jump to content

Determine changes to an object


NotionCommotion

Recommended Posts

The below PHP object and sub-objects all have methods.   After it is created, it is later modified by changing its property values and/or its sub-object property values as well as adding/deleting from the collections. 

How can one best determine what was changed?  Or more accurately, how can one best make a snapshot of it so changes can be identified?

One option is for the collection constructor's  to clone the content and store it as $this->initial and  for the other objects to create an array with individual properties and store it in $this->initial.  Or maybe for the parent object to create a JSON representative of all of it?  I then can easily use the various array functions to determine the difference.

Thank you

{
	"property1": 1,
	"property2": 1,
	"property3": 1,
	"collection1": {
		"content": [{
				"property4": "foo",
				"property5": "blabla"
			}, {
				"property4": "foo",
				"property5": "blabla"
			}, {
				"property4": "foo",
				"property5": "blabla"
			}
		],
		"property6": 123
	},
	"collection2": {
		"content": [{
				"property4": "foo",
				"property5": "blabla"
			}, {
				"property4": "foo",
				"property5": "blabla"
			}, {
				"property4": "foo",
				"property5": "blabla"
			}
		],
		"property6": 123,
		"subCollection1": {
			"content": [{
					"property7": "foo",
					"property8": "blabla"
				}, {
					"property7": "foo",
					"property8": "blabla"
				}, {
					"property7": "foo",
					"property8": "blabla"
				}
			],
			"property9": 123
		}
	}
}

 

Link to comment
Share on other sites

Do you actually need to know the differences, or is it enough to know that it changed? What for? Do changes always happen through methods run on the parent object (eg, setters) or can someone make changes without the object knowing (eg, modifying a property directly)?

Link to comment
Share on other sites

32 minutes ago, requinix said:

What for?

Probably because I am doing something that I shouldn't be doing.  For better or worse, I wasn't going to start using Doctrine or similar right now, and this was my attempt of building my own reverse mapper.

SubObjectMapper::read() queries the database using ORDER BY position, wraps each result in a class, and returns the array of objects to the parent mapper.  ParentObjectMapper::read()  wraps these objects as some other values in the main class, and returns the object to the service. 

The service then invokes some method on this object (or the sub-objects by using a getter) which will update a property value, add or remove a collection item, swap positions of two collection items, etc.

Originally, I was going to just have individual methods in the mappers for each of these activities, but thought it might be cleaner letting the service do whatever to the object, and then having the mapper ask the object what has been changed, and update the database  as applicable.  To determine what has been changed, the constructors for ParentEntity,  SeriesCollection, and CategoriesCollection (and similar objects in the the two collection classes) would store the initial state, and use this to return the changes.

Going about it wrong?

 

public function read($id) {
    $sql='SELECT a,b,c FROM t WHERE id=?';
    $stmt=$this->pdo->prepare($sql);
    $stmt->execute([$this->properties['themesId']]);
    $return new ParentEntity(
        $stmt->fetch(),
        new Entities\SeriesCollection(...$this->seriesMapper->read()),
        new Entities\CategoriesCollection(...$this->categoryMapper->read())
    );
}

 

Link to comment
Share on other sites

Well, that is reassuring.

I am assuming you mean storing the original data received by each entity constructor and that each object and sub-object should be responsible to store their own data.

Doing so will be easy enough for ParentEntity as well as for each of the individual Entities\CategoryNode objects in the arrays returned by categoryMapper->read() (shown below, and similar for series).  Where I am struggling is how to deal with CategoriesCollection.  It is extended from Collection shown at the bottom and receives an array of Nodes and doesn't receive "original data" the same way as the other classes.  As an array, it is not passed by reference so I can copy it using $this->blueprintbut but it will be a copy of the same referenced node objects.

Much appreciated.

    public function read() {
        //initiatate query...
        $collection=[];
        foreach($stmt as $rs) {
            $collection[]=new Entities\CategoryNode($rs);
        }
        return $collection;
    }


 

abstract class Collection implements \JsonSerializable, \ArrayAccess, \IteratorAggregate , CollectionInterface
{
    protected $container=[];
    protected $blueprint;

    public function __construct(Node ...$nodes) { //$nodes is an array of Node objects
        $this->container=$nodes;
        $this->blueprint=$nodes;
    }

    public function jsonSerialize() {
        $arr=[];
        foreach($this->container as $node) $arr[]=$node;
        return $arr;
    }

    public function getIterator() {
        return new \ArrayIterator($this->container);
    }

    public function hasContent() {
        return !empty($this->container);
    }

    public function offsetSet($offset, $value) {
        if (is_null($offset)) {
            $this->container[] = $value;
        }
        elseif(is_int($offset) || ctype_digit($offset)) {
            $this->container[$offset] = $value;
        }
        else throw new CollectionException('Offset must be an integer');
    }

    public function offsetExists($offset) {
        if(!is_int($offset) && !ctype_digit($offset)) throw new CollectionException('Offset must be an integer');
        return isset($this->container[$offset]);
        //return array_key_exists($offset, $this->container);
    }

    public function offsetUnset($offset) {
        if(!is_int($offset) && !ctype_digit($offset)) throw new CollectionException('Offset must be an integer');
        if(!isset($this->container[$offset])) throw new CollectionException('Offset does not exist');
        unset($this->container[$offset]);
        $this->container=array_values($this->container);
    }

    public function offsetGet($offset) {
        return isset($this->container[$offset]) ? $this->container[$offset] : null;
        //return $this->_data[$offset];
    }

    public function update($newValues) {
        // Common charts need $newValues->categories=[]
        if(count($this->container)!==count($newValues)) {
            throw new CollectionException('Collection counts do not match.');
        }
        foreach($this->container as $key=>$item) {
            $item->update($newValues[$key]);
        }
        return $this;
    }

    public function getChanges(){
        $add=[];
        $diff=[];
        foreach($this->container as $node) {
            $changes=$node->getChanges();
            $add[]=$changes['add'];
            $diff[]=$changes['diff'];
        }
        return ['add'=>$add, 'diff'=>$diff];
    }

    /**
    * Returns an array who's index is the position array given unique
    *
    * @param string $prop
    * @return array
    */
    public function getPositionChanges($prop){
        return array_udiff_assoc($this->container, $this->blueprint,
            function ($stack, $originalStack) {
                return is_numeric($stack->$prop) && is_numeric($originalStack->$prop)
                ?$stack->$prop - $originalStack->$prop
                :strcmp($stack->$prop, $originalStack->$prop);
            }
        );
    }

    public function getAll(){
        $this->container;
    }

    public function getIdByPosition($position){
        return $this->container[$position]->id;
    }

    public function move(int $initialPosition, int $finalPosition){
        if(!isset($this->container[$initialPosition]) || !isset($this->container[$finalPosition])) throw new CollectionException('Index does not exist');
        $node=$this->container[$initialPosition];
        unset($this->container[$initialPosition]);
        array_splice($this->container, $finalPosition, 0, $node);
        $this->container=array_values($this->container);
        //?? return $this->container;
    }

}

 

Link to comment
Share on other sites

The data it receives is the objects in the collections. If there are initial members of the collection then you'll need to track that, but otherwise the changes are what objects are added and removed.

Remember the whole process is recursive, so even if an object originally in the collection is not removed, it may still have been modified. Applies to any class that can contain child objects.

Link to comment
Share on other sites

Thanks requnix,  How would you determine whether an object has been added or removed?  Please see Collection::getChanges().

 

class ChartMapper extends Mappers
{
    public function update($obj) {
        $changes=$obj->getChanges(false);
        //Update the database as applicable
        $this->seriesMapper->update($obj->getSeriesCollection());
        $this->categoriesMapper->update($obj->getCategoriesCollection());
    }
}
class CategoryMapper extends Mappers
{
    public function update(Collection $collection) {
        $changes=$collection->getChanges(false);// [ 'added'=>[Node], 'deleted'=>[Node], 'changed'=>[Node] ]
        //Update the database as applicable
    }
}
class CategoriesCollection extends Collection {}
abstract class Collection
{
    protected $container;
    protected $blueprint;

    public function __construct(Node ...$nodes) {
        $this->container=$nodes;
        $this->blueprint=$nodes;
    }

    public function getChanges($getChildChanges=false){
        //Obviously the following will not work
        $added=array_diff($this->container, $this->blueprint);
        $removed=array_diff($this->blueprint, $this->container);
        $common=array_intersect($this->blueprint, $this->container);
        $changed=[];
        foreach($common as $node) {
            if($changes=$node->getChanges($getChildChanges)) {
                $changed[]=$changes;
            }
        }
        return ['added'=>$added, 'removed'=>$removed, 'changed'=>$changed];
    }
}
class ChartEntity extends Entities{

    public function __construct(array $properties, Entities\SeriesCollection $seriesCollection, Entities\CategoriesCollection $categoriesCollection) {
        $this->setValues($properties);
        $this->seriesCollection=$seriesCollection;
        $this->categoriesCollection=$categoriesCollection;
        $this->blueprint=$properties;
    }
    protected function getClassPropertyNames(){
        return ['id', 'idPublic', 'name', 'type', 'themesId', 'theme', 'config', 'config_default', 'typeName', 'masterType', 'title', 'subtitle', 'xAxis', 'yAxis'];
    }

    public function getChanges($getChildChanges=false){
        $changes=parent::getChanges();
        if($getChildChanges) {
            $changes['series']=$this->seriesCollection->getChanges($getChildChanges);
            $changes['categories']=$this->categoriesCollection->getChanges($getChildChanges);
        }
        return $changes;
    }
}
class CategoryNode extends Entity
{
    protected function getClassPropertyNames(){
        return ['id', 'name', 'position'];
    }
}
abstract class Entity
{
    protected $blueprint;

    public function __construct(array $properties) {
        $this->setValues($properties);
        $this->blueprint=$properties;
    }

    abstract protected function getClassPropertyNames();
    protected function setValues(array $properties) {
        $propertyNames=array_keys($properties);
        $expectedProperties=$this->getClassPropertyNames();
        if($error=array_diff($expectedProperties,$propertyNames)) {
            throw new \Exception("Values for ".implode(', ',$error)." must be provided");
        }
        if($error=array_diff($propertyNames, $expectedProperties)) {
            throw new \Exception("Unexpected properties ".implode(', ',$error)." were received");
        }
        foreach($properties as $name=>$value) {
            $this->name=$value;
        }
    }

    public function getChanges($getChildChanges=false){
        $current=[];
        foreach($this->getClassPropertyNames() as $name) {
            $current[$name]=$this->$name;
        }
        return array_diff_assoc($current, $this->blueprint);
    }

    //__get and __set...
}

 

Link to comment
Share on other sites

Archived

This topic is now archived and is closed to further replies.

×
×
  • Create New...

Important Information

We have placed cookies on your device to help make this website better. You can adjust your cookie settings, otherwise we'll assume you're okay to continue.