public function test_compile() { $closure = PropertyPath::compile('id'); $item = array('id' => 8); $this->assertTrue(\Sledgehammer\is_closure($closure), 'compile() should return a closure'); $this->assertSame(8, $closure($item)); }
/** * 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; } }
/** * Return a new collection sorted by the given field in ascending order. * * @param string|Closure $selector * @param int $method The sorting method, options are: SORT_REGULAR, SORT_NUMERIC, SORT_STRING or SORT_NATURAL * * @return Collection */ public function orderBy($selector, $method = SORT_REGULAR) { $sortOrder = []; $items = []; $indexed = true; $counter = 0; if (\Sledgehammer\is_closure($selector)) { $closure = $selector; } else { $closure = PropertyPath::compile($selector); } // Collect values foreach ($this as $key => $item) { $items[$key] = $item; $sortOrder[$key] = $closure($item, $key); if ($key !== $counter) { $indexed = false; } ++$counter; } // Sort the values if ($method === SORT_NATURAL) { natsort($sortOrder); } elseif ($method === \Sledgehammer\SORT_NATURAL_CI) { natcasesort($sortOrder); } else { asort($sortOrder, $method); } $sorted = []; foreach (array_keys($sortOrder) as $key) { if ($indexed) { $sorted[] = $items[$key]; } else { // Keep keys intact $sorted[$key] = $items[$key]; } } return new self($sorted); }