示例#1
0
 /**
  * Return a new collection where each element is a subselection of the original element.
  * (Known as "collect" in Ruby or "pluck" in underscore.js).
  *
  * @param string|array|Closure $selector  Path to the variable to select. Examples: "->id", "[message]", "customer.name", array('id' => 'message_id', 'message' => 'message_text')
  * @param string|null|false|Closure $selectKey (optional) The path that will be used as key. false: Keep the current key, null:  create linear keys.
  *
  * @return Collection
  */
 public function select($selector, $selectKey = false)
 {
     if (\Sledgehammer\is_closure($selector)) {
         $closure = $selector;
     } elseif (is_array($selector)) {
         $closure = function ($item) use($selector) {
             $target = [];
             PropertyPath::map($item, $target, $selector);
             return $target;
         };
     } else {
         $closure = PropertyPath::compile($selector);
     }
     if ($selectKey === null) {
         $index = 0;
     } elseif ($selectKey !== false && is_string($selectKey)) {
         $selectKey = PropertyPath::compile($selectKey);
     }
     $items = [];
     foreach ($this as $key => $item) {
         if ($selectKey === null) {
             $key = $index;
             ++$index;
         } elseif ($selectKey !== false) {
             $key = $selectKey($item);
         }
         $items[$key] = $closure($item, $key);
     }
     return new self($items);
 }
示例#2
0
 /**
  * Store the instance.
  *
  * @param string   $model
  * @param stdClass $instance
  * @param array    $options
  *                           'ignore_relations' => bool  true: Only save the instance,  false: Save all connected instances,
  *                           'add_unknown_instance' => bool, false: Reject unknown instances. (use $repository->create())
  *                           'reject_unknown_related_instances' => bool, false: Auto adds unknown instances
  *                           'keep_missing_related_instances' => bool, false: Auto deletes removed instances
  *                           }
  */
 public function save($model, $instance, $options = [])
 {
     $relationSaveOptions = $options;
     $relationSaveOptions['add_unknown_instance'] = value($options['reject_unknown_related_instances']) == false;
     $config = $this->_getConfig($model);
     if (is_object($instance) === false) {
         throw new Exception('Invalid parameter $instance, must be an object');
     }
     $index = $this->resolveIndex($instance, $config);
     $object = @$this->objects[$model][$index];
     if ($object === null) {
         foreach ($this->created[$config->name] as $createdIndex => $created) {
             if ($instance === $created) {
                 $index = $createdIndex;
                 $object = $this->objects[$model][$index];
                 break;
             }
         }
     }
     if ($object === null || $object['instance'] !== $instance) {
         if ($instance instanceof Junction) {
             throw new Exception('Can\'t save a Junction directly');
         }
         $resolvedModel = $this->resolveModel($instance);
         if ($model !== $resolvedModel) {
             throw new Exception('Can\'t save an "' . $resolvedModel . '" as an "' . $model . '"');
         }
         // id/index change-detection
         foreach ($this->objects[$model] as $object) {
             if ($object['instance'] === $instance) {
                 throw new Exception('Change rejected, the index changed from ' . $this->resolveIndex($object['data'], $config) . ' to ' . $index);
             }
         }
         throw new Exception('The instance is not bound to this Repository');
     }
     $rootSave = count($this->saving) === 0;
     if ($rootSave === false) {
         if (in_array($instance, $this->saving, true)) {
             // Recursion loop detected?
             return;
             // Prevent duplicate saves.
         }
     }
     $this->saving[] = $instance;
     $previousState = $object['state'];
     try {
         if ($object['state'] == 'saving') {
             throw new Exception('Object already in the saving state');
         }
         $this->objects[$model][$index]['state'] = 'saving';
         $this->_triggerEvent($instance, 'saving', $instance, $this);
         // Save belongsTo
         if (value($options['ignore_relations']) == false) {
             foreach ($config->belongsTo as $property => $belongsTo) {
                 if ($instance->{$property} !== null && $instance->{$property} instanceof BelongsToPlaceholder == false) {
                     $this->save($belongsTo['model'], $instance->{$property}, $relationSaveOptions);
                 }
             }
         }
         // Save instance
         $data = $this->convertToData($object['instance'], $config);
         if ($previousState == 'new') {
             $object['data'] = $this->_getBackend($config->backend)->add($data, $config->backendConfig);
             unset($this->created[$config->name][$index]);
             unset($this->objects[$config->name][$index]);
             $changes = array_diff_assoc($object['data'], $data);
             if (count($changes) > 0) {
                 // Has the data changed, for example by an auto-incremented id?
                 foreach ($changes as $column => $value) {
                     if (isset($config->readFilters[$column])) {
                         $value = \Sledgehammer\filter($value, $config->readFilters[$column]);
                     }
                     if (isset($config->properties[$column])) {
                         PropertyPath::set($config->properties[$column], $value, $instance);
                     }
                 }
             }
             $index = $this->resolveIndex($instance, $config);
             // @todo check if index already exists?
             $this->objects[$model][$index] = $object;
         } else {
             $this->objects[$model][$index]['data'] = $this->_getBackend($config->backend)->update($data, $object['data'], $config->backendConfig);
         }
         // Save hasMany
         if (value($options['ignore_relations']) == false) {
             foreach ($config->hasMany as $property => $hasMany) {
                 if ($instance->{$property} instanceof HasManyPlaceholder) {
                     continue;
                     // No changes (It's not even accessed)
                 }
                 $collection = $instance->{$property};
                 if ($collection instanceof Traversable) {
                     $collection = iterator_to_array($collection);
                 }
                 if ($collection === null) {
                     notice('Expecting an array for property "' . $property . '"');
                     $collection = [];
                 }
                 // Determine old situation
                 $old = @$this->objects[$model][$index]['hadMany'][$property];
                 if ($old === null && $previousState != 'new' && is_array($collection)) {
                     // Is the property replaced, before the placeholder was replaced?
                     // Load the previous situation
                     $oldValue = $instance->{$property};
                     $old = $this->resolveProperty($instance, $property, array('model' => $model, 'reload' => true))->toArray();
                     $instance->{$property} = $oldValue;
                 }
                 if (isset($hasMany['collection']['valueField'])) {
                     if (count(array_diff_assoc($old, $collection)) != 0) {
                         warning('Saving changes in complex hasMany relations are not (yet) supported.');
                     }
                     continue;
                 }
                 if (isset($hasMany['belongsTo'])) {
                     // One to Many?
                     $belongsToProperty = $hasMany['belongsTo'];
                     foreach ($collection as $key => $item) {
                         // Connect the items to the instance
                         if (is_object($item)) {
                             $item->{$belongsToProperty} = $instance;
                             if ($item instanceof BelongsToPlaceholder) {
                                 $replacedItem = $this->resolveInstance($item, $this->_getConfig($hasMany['model']));
                                 if ($replacedItem->{$belongsToProperty} !== $instance) {
                                     throw new Exception('Invalid placeholder in "' . $model . '->' . $property . '"');
                                 }
                                 $collection[$key] = $replacedItem;
                                 $item = $replacedItem;
                             }
                             $this->save($hasMany['model'], $item, $relationSaveOptions);
                         } elseif ($item !== array_value($old, $key)) {
                             warning('Unable to save the change "' . $item . '" in ' . $config->name . '->' . $property . '[' . $key . ']');
                         }
                     }
                 } elseif (isset($hasMany['through'])) {
                     // Many to Many?
                     $hasManyConfig = $this->_getConfig($hasMany['model']);
                     // Save changes in the related items
                     foreach ($collection as $item) {
                         if ($item instanceof Junction) {
                             $item = $this->resolveInstance($item, $hasManyConfig);
                         }
                         $this->save($hasMany['model'], $item, $relationSaveOptions);
                     }
                     $junctionConfig = $this->junctions[$hasMany['through']];
                     $junctionBackend = $this->_getBackend($junctionConfig->backend);
                     $hasManyIdPath = $hasManyConfig->properties[$hasManyConfig->id[0]];
                     $oldJunctions = @$object['junctions'][$property];
                     if ($oldJunctions === null) {
                         if ($object['state'] === 'new') {
                             $oldJunctions = [];
                         } else {
                             $oldValue = $instance->{$property};
                             $old = $this->resolveProperty($instance, $property, array('model' => $model, 'reload' => true))->toArray();
                             $instance->{$property} = $oldValue;
                             $object = $this->objects[$model][$index];
                             $oldJunctions = $object['junctions'][$property];
                             if ($oldJunctions === null) {
                                 throw new Exception('Failed to determine previous junctions');
                             }
                         }
                     }
                     $junctions = [];
                     $id = PropertyPath::get($config->properties[$config->id[0]], $instance);
                     foreach ($collection as $key => $item) {
                         $hasManyId = PropertyPath::get($hasManyIdPath, $item);
                         $oldJunction = @$oldJunctions[$hasManyId];
                         $junction = array($hasMany['reference'] => $id, $hasMany['id'] => $hasManyId);
                         if ($item instanceof Junction) {
                             PropertyPath::map($item, $junction, $hasMany['fields']);
                         }
                         $junctions[$hasManyId] = $junction;
                         $junctionChanged = false;
                         if ($oldJunction === null) {
                             // New relation?
                             $junctionBackend->add($junction, $junctionConfig->backendConfig);
                             $junctionChanged = true;
                         } else {
                             if (count(array_diff($junction, $oldJunction)) != 0) {
                                 $junctionBackend->update($junction, $oldJunction, $junctionConfig->backendConfig);
                                 $junctionChanged = true;
                             }
                         }
                         if ($junctionChanged) {
                             // Update the $instance in the $item->hasMany collection.
                             foreach ($hasManyConfig->hasMany as $manyToManyProperty => $manyToMany) {
                                 if (isset($manyToMany['through']) && $manyToMany['through'] === $hasMany['through']) {
                                     if ($item->{$manyToManyProperty} instanceof HasManyPlaceholder) {
                                         break;
                                         // collection not loaded.
                                     }
                                     $manyToManyIndex = $this->resolveIndex($item, $hasManyConfig);
                                     $this->objects[$hasMany['model']][$manyToManyIndex]['junctions'][$manyToManyProperty][$id] = $junction;
                                     // Prevent adding / updating the junction twice.
                                     $manyToManyExists = false;
                                     foreach ($item->{$manyToManyProperty} as $manyToManyKey => $manyToManyItem) {
                                         $manyToManyInstance = $manyToManyItem instanceof Junction ? $this->resolveInstance($manyToManyItem, $config) : $manyToManyItem;
                                         if ($instance === $manyToManyInstance) {
                                             // Instance already exists in the relation?
                                             $manyToManyExists = true;
                                             break;
                                         }
                                     }
                                     if (count($manyToMany['fields']) != 0) {
                                         // Update the Junction values
                                         if ($manyToManyExists && $manyToManyItem instanceof Junction) {
                                             PropertyPath::map($junction, $manyToManyItem, array_flip($manyToMany['fields']));
                                         } else {
                                             $fields = [];
                                             PropertyPath::map($junction, $fields, array_flip($manyToMany['fields']));
                                             $junctionClass = isset($hasMany['junctionClass']) ? $hasMany['junctionClass'] : '\\Sledgehammer\\Junction';
                                             $manyToManyItem = new $junctionClass($instance, $fields, true);
                                         }
                                     } else {
                                         $manyToManyItem = $instance;
                                     }
                                     if ($oldJunction === null) {
                                         if ($manyToManyExists === false) {
                                             // Instance not found in the relation?
                                             // @todo Wrap in a Junction?
                                             $item->{$manyToManyProperty}[] = $manyToManyItem;
                                             // add instance to the collection/array.
                                         }
                                         $this->objects[$hasMany['model']][$manyToManyIndex]['hadMany'][$manyToManyProperty][] = $manyToManyItem;
                                     }
                                 }
                             }
                         }
                     }
                     $this->objects[$model][$index]['junctions'][$property] = $junctions;
                 } else {
                     notice('Unable to verify/update foreign key');
                     // @TODO: implement raw fk injection.
                 }
                 if (value($options['keep_missing_related_instances']) == false) {
                     // Delete items that are no longer in the relation
                     if ($old !== null) {
                         if ($collection === null && count($old) > 0) {
                             notice('Unexpected value: null for property "' . $property . '", expecting an array or Iterator');
                         }
                         foreach ($old as $key => $item) {
                             if (in_array($item, $collection, true) === false) {
                                 if (!empty($hasMany['through']) && !empty($hasMany['fields'])) {
                                     // Can't compare Junctions using a identity check
                                     $getJunctionId = PropertyPath::compile($hasManyIdPath);
                                     $oldId = $getJunctionId($item);
                                     foreach ($collection as $newItem) {
                                         $newId = $getJunctionId($newItem);
                                         if ($newId === $oldId) {
                                             continue 2;
                                         }
                                     }
                                 }
                                 if (is_object($item)) {
                                     if (empty($hasMany['through'])) {
                                         // one-to-many?
                                         $this->delete($hasMany['model'], $item);
                                         // Delete the related model
                                     } else {
                                         // Delete the junction (many-to-many)
                                         $data = array($hasMany['reference'] => PropertyPath::get($config->properties[$config->id[0]], $instance), $hasMany['id'] => PropertyPath::get($hasManyIdPath, $item));
                                         $junctionConfig = $this->junctions[$hasMany['through']];
                                         $junctionBackend = $this->_getBackend($junctionConfig->backend);
                                         $junctionBackend->delete($data, $junctionConfig->backendConfig);
                                         // Also remove the $instance from the $item->hasMany collection.
                                         foreach ($hasManyConfig->hasMany as $manyToManyProperty => $manyToMany) {
                                             if (isset($manyToMany['through']) && $manyToMany['through'] === $hasMany['through']) {
                                                 if ($item->{$manyToManyProperty} instanceof HasManyPlaceholder) {
                                                     break;
                                                     // collection not loaded.
                                                 }
                                                 foreach ($item->{$manyToManyProperty} as $manyToManyKey => $manyToManyItem) {
                                                     $manyToManyInstance = $manyToManyItem instanceof Junction ? $this->resolveInstance($manyToManyItem, $config) : $manyToManyItem;
                                                     if ($manyToManyInstance === $instance) {
                                                         // Instance found in the relation?
                                                         unset($item->{$manyToManyProperty}[$manyToManyKey]);
                                                         break;
                                                     }
                                                 }
                                                 $manyToManyIndex = $this->resolveIndex($item, $hasManyConfig);
                                                 foreach ($this->objects[$hasMany['model']][$manyToManyIndex]['hadMany'][$manyToManyProperty] as $manyToManyKey => $manyToManyItem) {
                                                     $manyToManyInstance = $manyToManyItem instanceof Junction ? $this->resolveInstance($manyToManyItem, $config) : $manyToManyItem;
                                                     if ($manyToManyInstance === $instance) {
                                                         // Instance found in the relation?
                                                         // Update backend data, so re-adding the connection will be detected.
                                                         unset($this->objects[$hasMany['model']][$manyToManyIndex]['hadMany'][$manyToManyProperty][$manyToManyKey]);
                                                         unset($this->objects[$hasMany['model']][$manyToManyIndex]['junctions'][$manyToManyProperty][$data[$hasMany['reference']]]);
                                                         break;
                                                     }
                                                 }
                                                 break;
                                             }
                                         }
                                     }
                                 } else {
                                     warning('Unable to remove item[' . $key . ']: "' . $item . '" from ' . $config->name . '->' . $property);
                                 }
                             }
                         }
                     }
                 }
                 $this->objects[$model][$index]['hadMany'][$property] = $collection;
             }
         }
         $this->objects[$model][$index]['state'] = 'saved';
         $this->_triggerEvent($instance, 'saved', $instance, $this);
     } catch (Exception $e) {
         if ($rootSave) {
             $this->saving = [];
             // reset saving array.
         }
         $this->objects[$model][$index]['state'] = $previousState;
         // @todo Or is an error state more appropriate?
         throw $e;
     }
     if ($rootSave) {
         $saved = count($this->saving);
         $this->saving = [];
         // reset saving array.
         return $saved;
     }
 }
示例#3
0
 public function test_map()
 {
     $source = array('deep' => array('nested' => 1), 'value' => 2);
     $target = [];
     $mapping = array('dn' => 'deep.nested', 'meta[value]' => 'value');
     PropertyPath::map($source, $target, $mapping);
     $this->assertSame(array('dn' => 1, 'meta' => array('value' => 2)), $target, '');
 }