/** * Determine PropertyType from on variable type. * * This is most of the remainder of ValueFactory that is still needed. * * - if the given $value is a Node object, type will be REFERENCE, unless * $weak is set to true which results in WEAKREFERENCE * - if the given $value is a DateTime object, the type will be DATE. * - if the $value is an empty array, the type is arbitrarily set to STRING * - if the $value is a non-empty array, the type of its first element is * chosen. * * Note that string is converted to date exactly if it matches the jcr * formatting spec for dates (sYYYY-MM-DDThh:mm:ss.sssTZD) according to * http://www.day.com/specs/jcr/2.0/3_Repository_Model.html#3.6.4.3%20From%20DATE%20To * * @param mixed $value The variable we need to know the type of * @param boolean $weak When a Node is given as $value this can be given * as true to create a WEAKREFERENCE. * * @return int One of the type constants * * @throws ValueFormatException if the type can not be determined */ public function determineType($value, $weak = false) { if (is_array($value)) { if (0 === count($value)) { // there is no value to determine the type on. we arbitrarily // chose string, which is what jackrabbit does as well. return PropertyType::STRING; } $value = reset($value); } return PropertyType::determineType($value, $weak); }
/** * Internally used to set the value of the property without any notification * of changes nor state change. * * @param mixed $value * @param string $type * * @return void * * @see Property::setValue() * * @private */ public function _setValue($value, $type = PropertyType::UNDEFINED) { if (is_null($value)) { $this->remove(); return; } if (!is_integer($type)) { // @codeCoverageIgnoreStart throw new InvalidArgumentException("The type has to be one of the numeric constants defined in PHPCR\\PropertyType. {$type}"); // @codeCoverageIgnoreEnd } if ($this->isNew()) { $this->isMultiple = is_array($value); } if (is_array($value) && !$this->isMultiple) { throw new ValueFormatException('Can not set a single value property (' . $this->name . ') with an array of values'); } //TODO: check if changing type allowed. /* * if ($type !== null && ! canHaveType($type)) { * throw new ConstraintViolationException("Can not set this property to type ".PropertyType::nameFromValue($type)); * } */ if (PropertyType::UNDEFINED === $type) { $type = PropertyType::determineType(is_array($value) ? reset($value) : $value); } $targettype = $this->type; if ($this->type !== $type) { /* TODO: find out with node type definition if the new type is allowed if (canHaveType($type)) { */ $targettype = $type; /* } else { //convert to an allowed type. if the current type is defined $targettype = $this->type; } */ } $value = PropertyType::convertType($value, $targettype, $type); if (PropertyType::BINARY === $targettype) { $stat = fstat($value); //TODO: read file into local context? fstat not available on all streams $this->length = $stat['size']; } $this->type = $targettype; $this->value = $value; }
/** * @expectedException \PHPCR\ValueFormatException */ public function testDetermineTypeNull() { PropertyType::determineType(null); }
/** * Determine PropertyType from on variable type. * * This is most of the remainder of ValueFactory that is still needed. * * - if the given $value is a Node object, type will be REFERENCE, unless * $weak is set to true which results in WEAKREFERENCE * - if the given $value is a DateTime object, the type will be DATE. * * Note that string is converted to date exactly if it matches the jcr * formatting spec for dates (sYYYY-MM-DDThh:mm:ss.sssTZD) according to * http://www.day.com/specs/jcr/2.0/3_Repository_Model.html#3.6.4.3%20From%20DATE%20To * * @param mixed $value The variable we need to know the type of * @param boolean $weak When a Node is given as $value this can be given * as true to create a WEAKREFERENCE. * * @return int One of the type constants * * @throws ValueFormatException if the type can not be determined */ public function determineType($value, $weak = false) { return PropertyType::determineType($value, $weak); }
/** * Initialize or update this object with raw data from backend. * * @param array $rawData in the format as returned from Jackalope\Transport\TransportInterface * @param boolean $update whether to initialize this object or update * @param boolean $keepChanges only used if $update is true, same as $keepChanges in refresh() * * @see Node::__construct() * @see Node::refresh() */ private function parseData($rawData, $update, $keepChanges = false) { //TODO: refactor to use hash array instead of stdClass struct if ($update) { // keep backup of old state so we can remove what needs to be removed $oldNodes = array_flip(array_values($this->nodes)); $oldProperties = $this->properties; } /* * we collect all nodes coming from the backend. if we update with * $keepChanges, we use this to update the node list rather than losing * reorders * * properties are easy as they are not ordered. */ $nodesInBackend = array(); foreach ($rawData as $key => $value) { $node = false; // reset to avoid trouble if (is_object($value)) { // this is a node. add it if if (!$update || !$keepChanges || isset($oldNodes[$key]) || !($node = $this->objectManager->getCachedNode($this->path . '/' . $key))) { // for all those cases, if the node was moved away or is deleted in current session, we do not add it if (!$this->objectManager->isNodeMoved($this->path . '/' . $key) && !$this->objectManager->isItemDeleted($this->path . '/' . $key)) { // otherwise we (re)load a node from backend but a child has been moved away already $nodesInBackend[] = $key; } } if ($update) { unset($oldNodes[$key]); } } else { //property or meta information /* Property type declarations start with :, the value then is * the type string from the NodeType constants. We skip that and * look at the type when we encounter the value of the property. * * If its a binary data, we only get the type declaration and * no data. Then the $value of the type declaration is not the * type string for binary, but the number of bytes of the * property - resp. array of number of bytes. * * The magic property ::NodeIteratorSize tells this node has no * children. Ignore that info for now. We might optimize with * this info once we do prefetch nodes. */ if (0 === strpos($key, ':')) { if ((is_int($value) || is_array($value)) && $key != '::NodeIteratorSize') { // This is a binary property and we just got its length with no data $key = substr($key, 1); if (!isset($rawData->{$key})) { $binaries[$key] = $value; if ($update) { unset($oldProperties[$key]); } if (isset($this->properties[$key])) { // refresh existing binary, this will only happen in update // only update length if (!($keepChanges && $this->properties[$key]->isModified())) { $this->properties[$key]->_setLength($value); if ($this->properties[$key]->isDirty()) { $this->properties[$key]->setClean(); } } } else { // this will always fall into the creation mode $this->_setProperty($key, $value, PropertyType::BINARY, true); } } } //else this is a type declaration //skip this entry (if its binary, its already processeed continue; } if ($update && array_key_exists($key, $this->properties)) { unset($oldProperties[$key]); $prop = $this->properties[$key]; if ($keepChanges && $prop->isModified()) { continue; } } elseif ($update && array_key_exists($key, $this->deletedProperties)) { if ($keepChanges) { // keep the delete continue; } else { // restore the property $this->properties[$key] = $this->deletedProperties[$key]; $this->properties[$key]->setClean(); // now let the loop update the value. no need to talk to ObjectManager as it // does not store property deletions } } switch ($key) { case 'jcr:index': $this->index = $value; break; case 'jcr:primaryType': $this->primaryType = $value; // type information is exposed as property too, although there exist more specific methods $this->_setProperty('jcr:primaryType', $value, PropertyType::NAME, true); break; case 'jcr:mixinTypes': // type information is exposed as property too, although there exist more specific methods $this->_setProperty($key, $value, PropertyType::NAME, true); break; // OPTIMIZE: do not instantiate properties until needed // OPTIMIZE: do not instantiate properties until needed default: if (isset($rawData->{':' . $key})) { /* * this is an inconsistency between jackrabbit and * dbal transport: jackrabbit has type name, dbal * delivers numeric type. * we should eventually fix the format returned by * transport and either have jackrabbit transport * do the conversion or let dbal store a string * value instead of numerical. */ $type = is_numeric($rawData->{':' . $key}) ? $rawData->{':' . $key} : PropertyType::valueFromName($rawData->{':' . $key}); } else { $type = PropertyType::determineType(is_array($value) ? reset($value) : $value); } $this->_setProperty($key, $value, $type, true); break; } } } if ($update) { if ($keepChanges) { // we keep changes. merge new nodes to the right place $previous = null; $newFromBackend = array_diff($nodesInBackend, array_intersect($this->nodes, $nodesInBackend)); foreach ($newFromBackend as $name) { $pos = array_search($name, $nodesInBackend); if (is_array($this->originalNodesOrder)) { // update original order to send the correct reorderings array_splice($this->originalNodesOrder, $pos, 0, $name); } if ($pos === 0) { array_unshift($this->nodes, $name); } else { // do we find the predecessor of the new node in the list? $insert = array_search($nodesInBackend[$pos - 1], $this->nodes); if (false !== $insert) { array_splice($this->nodes, $insert + 1, 0, $name); } else { // failed to find predecessor, add to the end $this->nodes[] = $name; } } } } else { // discard changes, just overwrite node list $this->nodes = $nodesInBackend; $this->originalNodesOrder = null; } foreach ($oldProperties as $name => $property) { if (!($keepChanges && $property->isNew())) { // may not call remove(), we dont want another delete with the backend to be attempted $this->properties[$name]->setDeleted(); unset($this->properties[$name]); } } // notify nodes that where not received again that they disappeared foreach ($oldNodes as $name => $index) { if ($this->objectManager->purgeDisappearedNode($this->path . '/' . $name, $keepChanges)) { // drop, it was not a new child if ($keepChanges) { // otherwise we overwrote $this->nodes with the backend $id = array_search($name, $this->nodes); if (false !== $id) { unset($this->nodes[$id]); } } } } } else { // new node loaded from backend $this->nodes = $nodesInBackend; } }
/** * Create a property, either from server data or locally * * To indicate a property has newly been created locally, make sure to pass * true for the $new parameter. In that case, you should pass an empty array * for $data and use setValue afterwards to let the type magic be handled. * Then multivalue is determined on setValue * * For binary properties, the value is the length of the data(s), not the * data itself. * * @param FactoryInterface $factory the object factory * @param array $data array with fields <tt>type</tt> * (integer or string from PropertyType) and <tt>value</tt> (data for * creating the property value - array for multivalue property) * @param string $path the absolute path of this item * @param Session $session the session instance * @param ObjectManager $objectManager the objectManager instance - the * caller has to take care of registering this item with the object * manager * @param boolean $new optional: set to true to make this property aware * its not yet existing on the server. defaults to false */ public function __construct(FactoryInterface $factory, array $data, $path, Session $session, ObjectManager $objectManager, $new = false) { parent::__construct($factory, $path, $session, $objectManager, $new); $this->wrapBinaryStreams = $session->getRepository()->getDescriptor(Repository::JACKALOPE_OPTION_STREAM_WRAPPER); if (empty($data) && $new) { return; } if (!isset($data['value'])) { throw new InvalidArgumentException("Can't create property at {$path} without any data"); } if (isset($data['type']) && PropertyType::UNDEFINED !== $data['type']) { $type = $data['type']; if (is_string($type)) { $type = PropertyType::valueFromName($type); } elseif (!is_numeric($type)) { // @codeCoverageIgnoreStart throw new RepositoryException("INTERNAL ERROR -- No valid type specified ({$type})"); // @codeCoverageIgnoreEnd } else { //sanity check. this will throw InvalidArgumentException if $type is not a valid type PropertyType::nameFromValue($type); } } else { // we are creating a node $type = PropertyType::determineType(is_array($data['value']) ? reset($data['value']) : $data['value']); } $this->type = $type; if ($type == PropertyType::BINARY && !$new) { // reading a binary property from backend, we do not get the stream immediately but just the size if (is_array($data['value'])) { $this->isMultiple = true; } $this->length = $data['value']; $this->value = null; return; } if (is_array($data['value'])) { $this->isMultiple = true; $this->value = array(); foreach ($data['value'] as $value) { $this->value[] = $this->valueConverter->convertType($value, $type); } } elseif (null !== $data['value']) { $this->value = $this->valueConverter->convertType($data['value'], $type); } else { // @codeCoverageIgnoreStart throw new RepositoryException('INTERNAL ERROR -- data[value] may not be null'); // @codeCoverageIgnoreEnd } }
/** * {@inheritDoc} * * @api */ public function canSetProperty($propertyName, $value, $throw = false) { $propDefs = $this->getPropertyDefinitions(); try { $type = PropertyType::determineType(is_array($value) ? reset($value) : $value); } catch (ValueFormatException $e) { if ($throw) { throw $e; } return false; } // check explicit matches first and keep wildcard definitions for later $wildcards = array(); foreach ($propDefs as $prop) { if ('*' == $prop->getName()) { $wildcards[] = $prop; } elseif ($propertyName == $prop->getName()) { if (is_array($value) != $prop->isMultiple()) { if ($prop->isMultiple()) { throw new ConstraintViolationException("The property definition is multivalued, but the value '{$value}' is not."); } if (is_array($value)) { throw c("The value {$value} is multivalued, but the property definition is not."); } } if (PropertyType::UNDEFINED == $prop->getRequiredType() || $type == $prop->getRequiredType()) { return true; } // try if we can convert. OPTIMIZE: would be nice to know without actually attempting to convert try { PropertyType::convertType($value, $prop->getRequiredType(), $type); return true; } catch (ValueFormatException $e) { // fall through and return false } if ($throw) { throw new ConstraintViolationException("The property '{$propertyName}' with value '{$value}' can't be converted to an existing type."); } return false; // if there is an explicit match, it has to fit } } // now check if any of the wildcards matches foreach ($wildcards as $prop) { if (is_array($value) != $prop->isMultiple()) { continue; } if (PropertyType::UNDEFINED == $prop->getRequiredType() || $type == $prop->getRequiredType()) { return true; } // try if we can convert. OPTIMIZE: would be nice to know without actually attempting to convert try { PropertyType::convertType($value, $prop->getRequiredType(), $type); return true; } catch (ValueFormatException $e) { if ($throw) { throw $e; } return false; // if there is an explicit match, it has to fit } } if ($throw) { $val = is_object($value) ? get_class($value) : (is_scalar($value) ? (string) $value : gettype($value)); throw new ConstraintViolationException("Node type definition does not allow to set the property with name '{$propertyName}' and value '{$val}'"); } return false; }