diff --git a/README.md b/README.md index 00f53fe..27dfeb4 100644 --- a/README.md +++ b/README.md @@ -28,36 +28,35 @@ NOTE: If you are using PHPCR inside of Symfony, the DoctrinePHPCRBundle provides the commands inside the normal Symfony console and you don't need to prepare anything special. -* ``phpcr:workspace:create ``: Create the workspace name in the configured repository -* ``phpcr:register-node-types --allow-update [cnd-file]``: Register namespaces and node types from a "Compact Node Type Definition" .cnd file -* ``phpcr:dump [--sys_nodes[="..."]] [--props[="..."]] [path]``: Show the node names - under the specified path. If you set sys_nodes=yes you will also see system nodes. - If you set props=yes you will additionally see all properties of the dumped nodes. -* ``phpcr:purge``: Remove all content from the configured repository in the - configured workspace -* ``phpcr:sql2``: Run a query in the JCR SQL2 language against the repository and dump - the resulting rows to the console. - -**TODO:** - -* Implement commands for phpcr:import and phpcr:export to import and export the - PHPCR document view and system view XML dumps. -* Implement a simple .cnd parser in PHP and use it to make register-node-types - work with all repositories - +To get a list of the available commands, run `bin/phpcr` or set the commands up +in your application. Running `bin/phpcr help ` outputs the +documentation of that command. ## Helper Classes The helper classes provide implementations for basic common tasks to help users -and implementors of PHPCR. They are all in the namespace PHPCR\Util +and implementers of PHPCR. They are all in the namespace PHPCR\Util +### PathHelper -### TraversingItemVisitor +Used to manipulate paths. Implementations are recommended to use this, and +applications also profit from it. Using `dirname` and similar file system +operations on paths is not compatible with Microsoft Windows systems, thus you +should always use the methods in PathHelper. -This ``ItemVisitorInterface`` implementation is a basic implementation of crawling -a PHPCR tree. You can extend it to define what it should do while crawling the -tree. +### NodeHelper + +This helper has some generally useful methods like one to generate empty +`nt:unstructured` nodes to make sure a parent path exists. It also provides +some useful helper methods for implementations. + +### UUIDHelper +This little helper is mainly of interest for PHPCR implementers. It generates +valid *Universally Unique IDs* and can determine whether a given string is a +valid UUID. +We recommend all implementations to use this implementation to guarantee +consistent behaviour. ### QOM QueryBuilder @@ -66,25 +65,15 @@ The ``QueryBuilder`` is a fluent query builder with method names matching the on top of the QOM factory. It is the easiest way to programmatically build a PHPCR query. - ### Query Object Model Converter In the PHPCR\Util\QOM namespace we provide, implementation-independant code to convert between SQL2 and QOM. ``Sql2ToQomQueryConverter`` parses SQL2 queries into QOM . ``QomToSql2QueryConverter`` generates SQL2 out of a QOM. +### TraversingItemVisitor -### UUIDHelper - -This little helper is mainly of interest for PHPCR implementors. It generates -valid *Universally Unique IDs* and can determine wheter a given string is a -valid UUID. -We recommend all implementations to use this implementation to guarantee -constistent behaviour. - - -# TODO - -Move tests about the query converter from phpcr-api-tests tests/06_Query/QOM to -the tests folder. How to do the tests without a QOM factory implementation? +This ``ItemVisitorInterface`` implementation is a basic implementation of crawling +a PHPCR tree. You can extend it to define what it should do while crawling the +tree. diff --git a/src/PHPCR/Util/Console/Command/BaseCommand.php b/src/PHPCR/Util/Console/Command/BaseCommand.php index eeadd0b..105c44b 100644 --- a/src/PHPCR/Util/Console/Command/BaseCommand.php +++ b/src/PHPCR/Util/Console/Command/BaseCommand.php @@ -2,18 +2,19 @@ namespace PHPCR\Util\Console\Command; +use PHPCR\SessionInterface; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; use PHPCR\Util\Console\Helper\PhpcrCliHelper; -use Symfony\Component\Console\Input\InputOption; abstract class BaseCommand extends Command { protected $phpcrCliHelper; /** - * @return PHPCR\SessionInterface + * @return SessionInterface */ protected function getPhpcrSession() { @@ -21,7 +22,7 @@ protected function getPhpcrSession() } /** - * @return PHPCR\Util\Console\Helper\PhpcrCliHelper + * @return PhpcrCliHelper */ protected function getPhpcrCliHelper() { diff --git a/src/PHPCR/Util/Console/Command/NodeDumpCommand.php b/src/PHPCR/Util/Console/Command/NodeDumpCommand.php index fdc5d3a..251a0e7 100644 --- a/src/PHPCR/Util/Console/Command/NodeDumpCommand.php +++ b/src/PHPCR/Util/Console/Command/NodeDumpCommand.php @@ -21,20 +21,18 @@ namespace PHPCR\Util\Console\Command; -use PHPCR\Util\UUIDHelper; use PHPCR\ItemNotFoundException; use PHPCR\RepositoryException; use PHPCR\PathNotFoundException; + +use PHPCR\Util\UUIDHelper; +use PHPCR\Util\Console\Helper\PhpcrConsoleDumperHelper; + use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; -use PHPCR\Util\TreeWalker; -use PHPCR\Util\Console\Helper\TreeDumper\ConsoleDumperNodeVisitor; -use PHPCR\Util\Console\Helper\TreeDumper\ConsoleDumperPropertyVisitor; -use PHPCR\Util\Console\Helper\TreeDumper\SystemNodeFilter; - /** * Command subtrees under a path to the console * @@ -43,13 +41,6 @@ */ class NodeDumpCommand extends BaseCommand { - /** - * Limit after which to cut lines when dumping properties - * - * @var int - */ - private $dump_max_line_length = 120; - /** * {@inheritDoc} */ @@ -57,7 +48,7 @@ protected function configure() { $this ->setName('phpcr:node:dump') - ->addOption('sys_nodes', null, InputOption::VALUE_NONE, 'Also dump system nodes') + ->addOption('sys-nodes', null, InputOption::VALUE_NONE, 'Also dump system nodes (recommended to use with a depth limit)') ->addOption('props', null, InputOption::VALUE_NONE, 'Also dump properties of the nodes') ->addOption('identifiers', null, InputOption::VALUE_NONE, 'Also output node UUID') ->addOption('depth', null, InputOption::VALUE_OPTIONAL, 'Limit how many level of children to show', "-1") @@ -72,29 +63,20 @@ protected function configure() displayed as yaml arrays. By default the command filters out system nodes and properties (i.e. nodes and -properties with names starting with 'jcr:'), the sys_nodes option +properties with names starting with 'jcr:'), the --sys-nodes option allows to turn this filter off. HERE ) ; } - /** - * Change at which length lines in the dump get cut. - * - * @param int $length maximum line length after which to cut the output. - */ - public function setDumpMaxLineLength($length) - { - $this->dump_max_line_length = $length; - } - /** * {@inheritDoc} */ protected function execute(InputInterface $input, OutputInterface $output) { $session = $this->getPhpcrSession(); + /** @var $dumperHelper PhpcrConsoleDumperHelper */ $dumperHelper = $this->getHelper('phpcr_console_dumper'); // node to dump diff --git a/src/PHPCR/Util/NodeHelper.php b/src/PHPCR/Util/NodeHelper.php index 3332f8f..8244fe2 100644 --- a/src/PHPCR/Util/NodeHelper.php +++ b/src/PHPCR/Util/NodeHelper.php @@ -23,6 +23,7 @@ use PHPCR\ItemInterface; use PHPCR\NodeInterface; +use PHPCR\PropertyInterface; use PHPCR\PropertyType; use PHPCR\SessionInterface; use PHPCR\RepositoryException; @@ -50,7 +51,7 @@ private function __construct() * @param SessionInterface $session the PHPCR session to create the path * @param string $path full path, like /content/jobs/data * - * @return \PHPCR\NodeInterface the last node of the path, i.e. data + * @return NodeInterface the last node of the path, i.e. data */ public static function createPath(SessionInterface $session, $path) { @@ -87,12 +88,14 @@ public static function purgeWorkspace(SessionInterface $session) { $root = $session->getRootNode(); + /** @var $property PropertyInterface */ foreach ($root->getProperties() as $property) { if (! self::isSystemItem($property)) { $property->remove(); } } + /** @var $node NodeInterface */ foreach ($root->getNodes() as $node) { if (! self::isSystemItem($node)) { $node->remove(); @@ -111,13 +114,17 @@ public static function deleteAllNodes(SessionInterface $session) } /** - * Determine whether this item has a namespace that is to be considered - * a system namespace + * Determine whether this item is to be considered a system item that you + * usually want to hide and that should not be removed when purging the + * repository. * * @param ItemInterface $item */ public static function isSystemItem(ItemInterface $item) { + if ($item->getDepth() > 1) { + return false; + } $name = $item->getName(); return strpos($name, 'jcr:') === 0 || strpos($name, 'rep:') === 0; diff --git a/src/PHPCR/Util/PathHelper.php b/src/PHPCR/Util/PathHelper.php index 63c34f2..4defb79 100644 --- a/src/PHPCR/Util/PathHelper.php +++ b/src/PHPCR/Util/PathHelper.php @@ -36,9 +36,11 @@ class PathHelper /** * Do not create an instance of this class */ + // @codeCoverageIgnoreStart private function __construct() { } + // @codeCoverageIgnoreEnd /** * Check whether this is a syntactically valid absolute path. @@ -66,19 +68,10 @@ public static function assertValidAbsolutePath($path, $destination = false, $thr || strlen($path) > 1 && '/' === $path[strlen($path) - 1] || preg_match('-//|/\./|/\.\./-', $path) ) { - if ($throw) { - throw new RepositoryException("Invalid path $path"); - } - - return false; + return self::error("Invalid path $path", $throw); } if ($destination && ']' === $path[strlen($path) - 1]) { - if ($throw) { - throw new RepositoryException("Destination path may not end with index $path"); - } - - return false; - + return self::error("Destination path may not end with index $path", $throw); } return true; @@ -95,7 +88,8 @@ public static function assertValidAbsolutePath($path, $destination = false, $thr * encode and decode characters that are not natively allowed by a storage * engine. * - * @param string $name The name to check + * @param string $name The name to check + * @param boolean $throw whether to throw an exception on validation errors. * * @return bool true if valid, false if not valid and $throw was false * @@ -106,11 +100,11 @@ public static function assertValidAbsolutePath($path, $destination = false, $thr public static function assertValidLocalName($name, $throw = true) { if ('.' == $name || '..' == $name) { - throw new RepositoryException('Name may not be parent or self identifier: ' . $name); + return self::error("Name may not be parent or self identifier: $name", $throw); } if (preg_match('/\\/|:|\\[|\\]|\\||\\*/', $name)) { - throw new RepositoryException('Name contains illegal characters: '.$name); + return self::error("Name contains illegal characters: $name", $throw); } return true; @@ -141,24 +135,18 @@ public static function assertValidLocalName($name, $throw = true) public static function normalizePath($path, $destination = false, $throw = true) { if (!is_string($path)) { - throw new RepositoryException('Expected string but got ' . gettype($path)); + return self::error('Expected string but got ' . gettype($path), $throw); } - if (strlen($path) === 0) { - throw new RepositoryException('Path must not be of zero length'); + return self::error('Path must not be of zero length', $throw); } if ('/' === $path) { - return '/'; } if ('/' !== $path[0]) { - if ($throw) { - throw new RepositoryException("Not an absolute path '$path'"); - } - - return false; + return self::error("Not an absolute path '$path'", $throw); } $finalParts= array(); @@ -211,9 +199,16 @@ public static function normalizePath($path, $destination = false, $throw = true) */ public static function absolutizePath($path, $context, $destination = false, $throw = true) { - if (! $path) { - throw new RepositoryException('empty path'); + if (!is_string($path)) { + return self::error('Expected string path but got ' . gettype($path), $throw); + } + if (!is_string($context)) { + return self::error('Expected string context but got ' . gettype($context), $throw); } + if (strlen($path) === 0) { + return self::error('Path must not be of zero length', $throw); + } + if ('/' !== $path[0]) { $path = ('/' === $context) ? "/$path" : "$context/$path"; } @@ -270,4 +265,24 @@ public static function getPathDepth($path) { return substr_count(rtrim($path, '/'), '/'); } + + /** + * If $throw is true, throw a RepositoryException with $msg. Otherwise + * return false. + * + * @param string $msg the exception message to use in case of throw being true + * @param boolean $throw whether to throw the exception or return false + * + * @return boolean false + * + * @throws RepositoryException + */ + private static function error($msg, $throw) + { + if ($throw) { + throw new RepositoryException($msg); + } + + return false; + } } diff --git a/tests/PHPCR/Tests/Util/Console/Command/BaseCommandTest.php b/tests/PHPCR/Tests/Util/Console/Command/BaseCommandTest.php index e63f4aa..fe4b96b 100644 --- a/tests/PHPCR/Tests/Util/Console/Command/BaseCommandTest.php +++ b/tests/PHPCR/Tests/Util/Console/Command/BaseCommandTest.php @@ -2,6 +2,9 @@ namespace PHPCR\Tests\Util\Console\Command; +use PHPCR\NodeInterface; +use PHPCR\Query\QueryManagerInterface; +use PHPCR\Query\RowInterface; use PHPCR\RepositoryInterface; use Symfony\Component\Console\Application; use Symfony\Component\Console\Tester\CommandTester; @@ -18,33 +21,48 @@ abstract class BaseCommandTest extends \PHPUnit_Framework_TestCase { - /** - * @var SessionInterface|\PHPUnit_Framework_MockObject_MockObject + /** + * @var SessionInterface|\PHPUnit_Framework_MockObject_MockObject * */ public $session; - /** - * @var WorkspaceInterface|\PHPUnit_Framework_MockObject_MockObject + /** + * @var WorkspaceInterface|\PHPUnit_Framework_MockObject_MockObject */ public $workspace; - /** - * @var RepositoryInterface|\PHPUnit_Framework_MockObject_MockObject + /** + * @var RepositoryInterface|\PHPUnit_Framework_MockObject_MockObject */ public $repository; - /** - * @var PhpcrConsoleDumperHelper|\PHPUnit_Framework_MockObject_MockObject + /** + * @var PhpcrConsoleDumperHelper|\PHPUnit_Framework_MockObject_MockObject */ public $dumperHelper; - /** - * @var HelperSet + /** + * @var NodeInterface|\PHPUnit_Framework_MockObject_MockObject + */ + public $node1; + + /** + * @var RowInterface|\PHPUnit_Framework_MockObject_MockObject + */ + public $row1; + + /** + * @var QueryManagerInterface|\PHPUnit_Framework_MockObject_MockObject + */ + public $queryManager; + + /** + * @var HelperSet */ public $helperSet; - /** - * @var Application + /** + * @var Application */ public $application; @@ -107,7 +125,7 @@ public function executeCommand($name, $args, $status = 0) $args = array_merge(array( 'command' => $command->getName(), ), $args); - $this->assertEquals(0, $commandTester->execute($args)); + $this->assertEquals($status, $commandTester->execute($args)); return $commandTester; } diff --git a/tests/PHPCR/Tests/Util/Console/Command/NodeDumpCommandTest.php b/tests/PHPCR/Tests/Util/Console/Command/NodeDumpCommandTest.php index 0082e8b..ef3bd71 100644 --- a/tests/PHPCR/Tests/Util/Console/Command/NodeDumpCommandTest.php +++ b/tests/PHPCR/Tests/Util/Console/Command/NodeDumpCommandTest.php @@ -2,11 +2,18 @@ namespace PHPCR\Tests\Util\Console\Command; +use PHPCR\ItemNotFoundException; +use PHPCR\Util\UUIDHelper; use Symfony\Component\Console\Application; + +use PHPCR\Util\TreeWalker; use PHPCR\Util\Console\Command\NodeDumpCommand; class NodeDumpCommandTest extends BaseCommandTest { + /** @var TreeWalker|\PHPUnit_Framework_MockObject_MockObject */ + protected $treeWalker; + public function setUp() { parent::setUp(); @@ -17,15 +24,78 @@ public function setUp() public function testCommand() { - $this->dumperHelper->expects($this->once()) + $this->dumperHelper + ->expects($this->once()) + ->method('getTreeWalker') + ->will($this->returnValue($this->treeWalker)) + ; + $this->session + ->expects($this->once()) + ->method('getNode') + ->with('/') + ->will($this->returnValue($this->node1)) + ; + $this->treeWalker + ->expects($this->once()) + ->method('traverse') + ->with($this->node1) + ; + + $this->application->add(new NodeDumpCommand()); + + $this->executeCommand('phpcr:node:dump', array()); + } + + public function testCommandIdentifier() + { + $uuid = UUIDHelper::generateUUID(); + + $this->dumperHelper + ->expects($this->once()) ->method('getTreeWalker') - ->will($this->returnValue($this->treeWalker)); - $this->session->expects($this->once()) + ->will($this->returnValue($this->treeWalker)) + ; + $this->session + ->expects($this->once()) + ->method('getNodeByIdentifier') + ->with($uuid) + ->will($this->returnValue($this->node1)) + ; + $this->treeWalker + ->expects($this->once()) + ->method('traverse') + ->with($this->node1) + ; + + $this->application->add(new NodeDumpCommand()); + + $this->executeCommand('phpcr:node:dump', array('identifier' => $uuid)); + } + + public function testInvalidRefFormat() + { + $this->application->add(new NodeDumpCommand()); + + try { + $this->executeCommand('phpcr:node:dump', array('--ref-format' => 'xy')); + $this->fail('invalid ref-format did not produce exception'); + } catch (\Exception $e) { + // success + } + } + + public function testNotFound() + { + $this->session + ->expects($this->once()) ->method('getNode') - ->will($this->returnValue($this->node1)); + ->with('/') + ->will($this->throwException(new ItemNotFoundException())) + ; $this->application->add(new NodeDumpCommand()); - $ct = $this->executeCommand('phpcr:node:dump', array()); + $ct = $this->executeCommand('phpcr:node:dump', array(), 1); + $this->assertContains('does not exist', $ct->getDisplay()); } } diff --git a/tests/PHPCR/Tests/Util/PathHelperTest.php b/tests/PHPCR/Tests/Util/PathHelperTest.php index c1f4d8b..b27be47 100644 --- a/tests/PHPCR/Tests/Util/PathHelperTest.php +++ b/tests/PHPCR/Tests/Util/PathHelperTest.php @@ -28,6 +28,11 @@ public function testAssertValidPathIndexed() $this->assertTrue(PathHelper::assertValidAbsolutePath('/parent[7]/child')); } + public function testAssertValidPathIndexedAtEnd() + { + $this->assertTrue(PathHelper::assertValidAbsolutePath('/parent[7]/child[3]')); + } + /** * @expectedException \PHPCR\RepositoryException */ @@ -138,43 +143,40 @@ public function testNormalizePath($inputPath, $outputPath) public static function dataproviderNormalizePath() { return array( - array('/../foo', '/foo'), - array('/../', '/'), - array('/foo/../bar', '/bar'), - array('/foo/./bar', '/foo/bar'), + array('/', '/'), + array('/../foo', '/foo'), + array('/../', '/'), + array('/foo/../bar', '/bar'), + array('/foo/./bar', '/foo/bar'), ); } - /** - * @expectedException \PHPCR\RepositoryException - */ - public function testNormalizePathInvalid() - { - PathHelper::normalizePath('foo/bar'); - } - - /** - * @expectedException \PHPCR\RepositoryException - */ - public function testNormalizePathShortInvalid() + public static function dataproviderNormalizePathInvalid() { - PathHelper::normalizePath('bar'); + return array( + array('foo/bar'), + array('bar'), + array('/foo/bar/'), + array(''), + array(new \stdClass()), + ); } /** + * @dataProvider dataproviderNormalizePathInvalid * @expectedException \PHPCR\RepositoryException */ - public function testNormalizePathTrailing() + public function testNormalizePathInvalidThrow($input) { - PathHelper::normalizePath('/foo/bar/'); + PathHelper::normalizePath($input); } /** - * @expectedException \PHPCR\RepositoryException + * @dataProvider dataproviderNormalizePathInvalid */ - public function testNormalizePathEmpty() + public function testNormalizePathInvalidNoThrow($input) { - PathHelper::normalizePath(''); + $this->assertFalse(PathHelper::normalizePath($input, true, false)); } // absolutizePath tests @@ -197,6 +199,34 @@ public static function dataproviderAbsolutizePath() ); } + /** + * @expectedException \PHPCR\RepositoryException + * @dataProvider dataproviderAbsolutizePathInvalid + */ + public function testAbsolutizePathInvalidThrow($inputPath, $context, $target) + { + PathHelper::absolutizePath($inputPath, $context, $target); + } + + /** + * @dataProvider dataproviderAbsolutizePathInvalid + */ + public function testAbsolutizePathInvalidNoThrow($inputPath, $context, $target) + { + $this->assertFalse(PathHelper::absolutizePath($inputPath, $context, $target, false)); + } + + public static function dataproviderAbsolutizePathInvalid() + { + return array( + array('', '/context', false), + array(null, '/context', false), + array('foo', null, false), + array(new \stdClass(), '/context', false), + array('foo[2]', '/bar', true), + ); + } + // getParentPath tests public function testGetParentPath()