<?php
namespace Jackalope\NodeType;
use Iterator;
use IteratorAggregate;
use ArrayIterator;
use PHPCR\AccessDeniedException;
use PHPCR\NamespaceException;
use PHPCR\NamespaceRegistryInterface;
use PHPCR\NodeType\NodeTypeInterface;
use PHPCR\NodeType\NodeTypeDefinitionInterface;
use PHPCR\NodeType\NodeTypeManagerInterface;
use PHPCR\NodeType\NoSuchNodeTypeException;
use PHPCR\NodeType\NodeTypeExistsException;
use Jackalope\ObjectManager;
use Jackalope\NotImplementedException;
use Jackalope\FactoryInterface;
use PHPCR\RepositoryException;
/**
* {@inheritDoc}
*
* In Jackalope, we try to do lazy fetching of node types to reduce overhead.
* Jackalope supports registering node types, and when using the jackrabbit for
* transport, there is an additional method registerNodeTypesCnd for the
* jackrabbit specific textual node type specification.
*
* @license http://www.apache.org/licenses Apache License Version 2.0, January 2004
* @license http://opensource.org/licenses/MIT MIT License
*/
class NodeTypeManager implements IteratorAggregate, NodeTypeManagerInterface
{
/**
* The factory to instantiate objects.
*
* @var FactoryInterface
*/
protected $factory;
/**
* @var ObjectManager
*/
protected $objectManager;
/**
* @var NamespaceRegistryInterface
*/
protected $namespaceRegistry;
/**
* Cache of already fetched primary node type instances.
*
* @var array
*/
protected $primaryTypes;
/**
* Cache of already fetched mixin node type instances.
*
* @var array
*/
protected $mixinTypes;
/**
* Array of arrays with the super type as key and its sub types as values.
* @var array
*/
protected $nodeTree = [];
/**
* Flag to only load all node types from the backend once.
*
* Methods like hasNodeType() need to fetch all node types. Others like
* getNodeType() do not need all, but just the requested one. Unless we
* need all, we only load specific ones and cache them.
*
* @var boolean
*/
protected $fetchedAllFromBackend = false;
/**
* Create the node type manager for a session.
*
* There may be only one instance per session
* @param FactoryInterface $factory
* @param ObjectManager $objectManager
* @param NamespaceRegistryInterface $namespaceRegistry
*/
public function __construct(
FactoryInterface $factory,
ObjectManager $objectManager,
NamespaceRegistryInterface $namespaceRegistry
) {
$this->factory = $factory;
$this->objectManager = $objectManager;
$this->namespaceRegistry = $namespaceRegistry;
}
/**
* Fetch a node type from backend.
*
* Without a filter parameter, this will fetch all node types from the backend.
*
* It is no problem to call this method with null as name, it will remember
* once it fetched all node types and do nothing after that.
*
* On fetch all, already cached node types are kept.
*
* @param string $name type name to fetch. defaults to null which will
* fetch all nodes.
*/
protected function fetchNodeTypes($name = null)
{
if ($this->fetchedAllFromBackend) {
return;
}
if (null !== $name) {
if (!empty($this->primaryTypes[$name]) || !empty($this->mixinTypes[$name])) {
return; //we already know this node
}
//OPTIMIZE: also avoid trying to fetch nonexisting definitions we already tried to get
$nodeTypes = $this->objectManager->getNodeType($name);
} else {
$nodeTypes = $this->objectManager->getNodeTypes();
$this->fetchedAllFromBackend = true;
}
foreach ($nodeTypes as $nodeType) {
/** @var NodeType $nodeType */
$nodeType = $this->factory->get(NodeType::class, [$this, $nodeType]);
$name = $nodeType->getName();
//do not overwrite existing types. maybe they where changed locally
if (empty($this->primaryTypes[$name]) && empty($this->mixinTypes[$name])) {
$this->addNodeType($nodeType);
}
}
}
/**
* Stores the node type in our internal structures (flat && tree)
*
* @param NodeTypeInterface $nodeType The nodetype to add
*/
protected function addNodeType(NodeTypeInterface $nodeType)
{
if ($nodeType->isMixin()) {
$this->mixinTypes[$nodeType->getName()] = $nodeType;
} else {
$this->primaryTypes[$nodeType->getName()] = $nodeType;
}
$this->addToNodeTree($nodeType);
}
/**
* Helper method for node types: Returns the declared subtypes of a given
* nodename.
*
* @param string $nodeTypeName
*
* @return array with the names of the subnode types pointing to the node type instances
*
* @see NodeType::getDeclaredSubtypes
*
* @private
*/
public function getDeclaredSubtypes($nodeTypeName)
{
// OPTIMIZE: any way to avoid loading all nodes at this point?
$this->fetchNodeTypes();
if (empty($this->nodeTree[$nodeTypeName])) {
return [];
}
return $this->nodeTree[$nodeTypeName];
}
/**
* Helper method for NodeType: Returns all sub types of a node and their
* sub types.
*
* @param string $nodeTypeName
*
* @return array with the names of the subnode types pointing to the node type instances
*
* @see NodeType::getSubtypes
*
* @private
*/
public function getSubtypes($nodeTypeName)
{
// OPTIMIZE: any way to avoid loading all nodes at this point?
$this->fetchNodeTypes();
$ret = [];
if (isset($this->nodeTree[$nodeTypeName])) {
foreach ($this->nodeTree[$nodeTypeName] as $name => $subnode) {
$ret = array_merge($ret, [$name => $subnode], $this->getSubtypes($name));
}
}
return $ret;
}
/**
* Adds the declared super types of a node type to the tree to be able to
* fetch the sub types of those super types later on.
*
* Part of addNodeType.
*
* @param NodeTypeInterface $nodetype the node type to add.
*/
private function addToNodeTree($nodetype)
{
foreach ($nodetype->getDeclaredSupertypeNames() as $declaredSupertypeName) {
if (isset($this->nodeTree[$declaredSupertypeName])) {
$this->nodeTree[$declaredSupertypeName] =
array_merge(
$this->nodeTree[$declaredSupertypeName],
[$nodetype->getName() => $nodetype]
);
} else {
$this->nodeTree[$declaredSupertypeName] = [$nodetype->getName() => $nodetype];
}
}
}
/**
* {@inheritDoc}
*
* @api
*/
public function getNodeType($nodeTypeName)
{
if (null === $nodeTypeName) {
throw new NoSuchNodeTypeException('nodeTypeName is <null>');
}
if ('' === $nodeTypeName) {
throw new NoSuchNodeTypeException('nodeTypeName is empty string');
}
if ($nodeTypeName[0] === '{') {
list($uri, $name) = explode('}', substr($nodeTypeName, 1));
$prefix = $this->namespaceRegistry->getPrefix($uri);
$nodeTypeName = "$prefix:$name";
}
$this->fetchNodeTypes($nodeTypeName);
if (isset($this->primaryTypes[$nodeTypeName])) {
return $this->primaryTypes[$nodeTypeName];
}
if (isset($this->mixinTypes[$nodeTypeName])) {
return $this->mixinTypes[$nodeTypeName];
}
throw new NoSuchNodeTypeException($nodeTypeName);
}
/**
* {@inheritDoc}
*
* @api
*/
public function hasNodeType($name)
{
try {
$this->fetchNodeTypes($name);
} catch (NoSuchNodeTypeException $e) {
// if we have not yet fetched all types and this type is not existing
// we get an exception. just ignore the exception, we don't have the type.
return false;
}
return isset($this->primaryTypes[$name]) || isset($this->mixinTypes[$name]);
}
/**
* {@inheritDoc}
*
* @api
*/
public function getAllNodeTypes()
{
$this->fetchNodeTypes();
return new ArrayIterator(array_merge($this->primaryTypes, $this->mixinTypes));
}
/**
* {@inheritDoc}
*
* @api
*/
public function getPrimaryNodeTypes()
{
$this->fetchNodeTypes();
return new ArrayIterator($this->primaryTypes);
}
/**
* {@inheritDoc}
*
* @api
*/
public function getMixinNodeTypes()
{
$this->fetchNodeTypes();
return new ArrayIterator($this->mixinTypes);
}
/**
* {@inheritDoc}
*
* @api
*/
public function createNodeTypeTemplate($ntd = null)
{
return $this->factory->get(NodeTypeTemplate::class, [$this, $ntd]);
}
/**
* {@inheritDoc}
*
* @api
*/
public function createNodeDefinitionTemplate()
{
return $this->factory->get(NodeDefinitionTemplate::class, [$this]);
}
/**
* {@inheritDoc}
*
* @api
*/
public function createPropertyDefinitionTemplate()
{
return $this->factory->get(PropertyDefinitionTemplate::class, [$this]);
}
/**
* {@inheritDoc}
*
* @api
*/
public function registerNodeType(NodeTypeDefinitionInterface $ntd, $allowUpdate)
{
$this->registerNodeTypes([$ntd], $allowUpdate);
return $ntd;
}
/**
* Internally create a node type object
*
* @param NodeTypeDefinitionInterface $ntd
* @param bool $allowUpdate whether updating the definition is to be allowed or not
*
* @return NodeType the new node type
*
* @throws RepositoryException
* @throws NodeTypeExistsException
*/
protected function createNodeType(NodeTypeDefinitionInterface $ntd, $allowUpdate)
{
if ($this->hasNodeType($ntd->getName()) && !$allowUpdate) {
throw new NodeTypeExistsException('NodeType already existing: '.$ntd->getName());
}
return $this->factory->get(NodeType::class, [$this, $ntd]);
}
/**
* {@inheritDoc}
*
* @api
*/
public function registerNodeTypes(array $definitions, $allowUpdate)
{
$nts = [];
// prepare them first (all or nothing)
foreach ($definitions as $definition) {
$nts[$definition->getName()] = $this->createNodeType($definition, $allowUpdate);
}
$this->objectManager->registerNodeTypes($definitions, $allowUpdate);
// no need to fetch the node types as with cnd, we already have the def and can
// now register them ourselves
foreach ($nts as $nt) {
$this->addNodeType($nt);
}
return new ArrayIterator($nts);
}
/**
* {@inheritDoc}
*
* @throws AccessDeniedException
* @throws NamespaceException
*
* @api
*/
public function registerNodeTypesCnd($cnd, $allowUpdate)
{
//set fetched from backend to false to allow to load the new types from backend
$fetched = $this->fetchedAllFromBackend;
$this->fetchedAllFromBackend = false;
$this->objectManager->registerNodeTypesCnd($cnd, $allowUpdate);
//parse out type names and fetch types to return definitions of the new nodes
preg_match_all('/\[([^\]]*)\]/', $cnd, $names);
$types = [];
foreach ($names[1] as $name) {
$types[$name] = $this->getNodeType($name);
}
$this->fetchedAllFromBackend = $fetched;
return $types;
}
/**
* {@inheritDoc}
*
* @api
*/
public function unregisterNodeType($name)
{
if (!empty($this->primaryTypes[$name])) {
unset($this->primaryTypes[$name]);
} elseif (!empty($this->mixinTypes[$name])) {
unset($this->mixinTypes[$name]);
} else {
throw new NoSuchNodeTypeException('NodeType not found: '.$name);
}
throw new NotImplementedException('TODO: remove from nodeTree and register with server (jackrabbit has not implemented this yet)');
}
/**
* {@inheritDoc}
*
* @api
*/
public function unregisterNodeTypes(array $names)
{
foreach ($names as $name) {
$this->unregisterNodeType($name);
}
}
/**
* Provide Traversable interface: redirect to getAllNodeTypes
*
* @return Iterator over all node types
* @throws RepositoryException
*/
#[\ReturnTypeWillChange]
public function getIterator(): \Traversable
{
return $this->getAllNodeTypes();
}
}