diff --git a/.doctrine-project.json b/.doctrine-project.json new file mode 100644 index 0000000..98c76cc --- /dev/null +++ b/.doctrine-project.json @@ -0,0 +1,14 @@ +{ + "active": true, + "name": "PHPCR Bundle", + "slug": "phpcr-bundle", + "docsSlug": "doctrine-phpcr-bundle", + "versions": [ + { + "name": "3.x", + "branchName": "3.x", + "slug": "latest", + "current": true + } + ] +} diff --git a/README.md b/README.md index f0b2869..ec3a613 100644 --- a/README.md +++ b/README.md @@ -19,8 +19,7 @@ adds features of its own like multilanguage. ## Documentation -For information, see [Symfony CMF Documentation](http://symfony.com/doc/master/cmf/index.html), -specifically [The Database Layer: PHPCR-ODM](http://symfony.com/doc/master/cmf/book/database_layer.html) -and [DoctrinePHPCRBundle](http://symfony.com/doc/master/cmf/bundles/phpcr_odm/introduction.html). +For information on PHPCR-ODM, see [Doctrine Documentation](https://www.doctrine-project.org/projects/phpcr-odm.html), +and [DoctrinePHPCRBundle](https://www.doctrine-project.org/projects/doctrine-phpcr-bundle.html). -PHPCR-ODM in general is documented in the [Doctrine PHPCR-ODM documentation](http://docs.doctrine-project.org/projects/doctrine-phpcr-odm/en/latest/). +Read more about PHPCR, the storage layer behind PHPCR-ODM: [PHPCR documentation](https://phpcr.readthedocs.io/en/latest/). diff --git a/doc/configuration.rst b/doc/configuration.rst new file mode 100644 index 0000000..113fce5 --- /dev/null +++ b/doc/configuration.rst @@ -0,0 +1,720 @@ +Configuration Reference +======================= + +The DoctrinePHPCRBundle can be configured under the ``doctrine_phpcr`` key in +your application configuration. When using XML, you can use the +``http://doctrine-project.org/schema/symfony-dic/odm/phpcr`` namespace. + + +Configuration +------------- + +``session`` +~~~~~~~~~~~ + +.. tip:: + + You can also configure multiple session. See + :doc:`multiple_sessions` for details. + +.. configuration-block:: + + .. code-block:: yaml + + # app/config/config.yml + doctrine_phpcr: + session: + backend: + type: X + # optional parameters for Jackalope + parameters: + jackalope.factory: Jackalope\Factory + jackalope.check_login_on_server: false + jackalope.disable_stream_wrapper: false + jackalope.auto_lastmodified: true + # see below for how to configure the backend of your choice + workspace: default + username: admin + password: admin + # tweak options for Jackalope (all versions) + options: + jackalope.fetch_depth: 1 + + .. code-block:: xml + + + + + + + + + Jackalope\Factory + false + false + true + + + + + + + + .. code-block:: php + + // app/config/config.php + $container->loadFromExtension('doctrine_phpcr', [ + 'session' => [ + 'backend' => [ + 'type' => 'X', + 'parameters' => [ + 'jackalope.factory' => 'Jackalope\Factory', + 'jackalope.check_login_on_server' => false, + 'jackalope.disable_stream_wrapper' => false, + 'jackalope.auto_lastmodified' => true, + ], + ], + 'workspace' => 'default', + 'username' => 'admin', + 'password' => 'admin', + 'options' => [ + 'jackalope.fetch_depth' => 1, + ], + ], + ]); + +``workspace`` +""""""""""""" + +**type**: ``string`` **required** + +Defines the PHPCR workspace to use for this PHPCR session. + +.. tip:: + + Every PHPCR implementation should provide the workspace called *default*, + but you can choose a different one. There is the + ``doctrine:phpcr:workspace:create`` command to initialize a new workspace. + See also :ref:`bundles-phpcr-odm-commands`. + +``username and password`` +""""""""""""""""""""""""" + +**type**: ``string`` **default**: ``null`` + +These credentials are used on the PHPCR layer for the +``PHPCR\SimpleCredentials``. They are optional for jackalope doctrine-dbal. + +Do not confuse these credentials with the username and password used by +Doctrine DBAL to connect to the underlying RDBMS where the data +is actually stored. + +``backend type`` +"""""""""""""""" + +**type**: ``string`` **default**: ``jackrabbit`` + +This designates the PHPCR implementation. Valid options are + +* ``jackrabbit``; +* ``doctrinedbal``; +* ``prismic``. + +``backend parameters`` +"""""""""""""""""""""" + +If you are using one of the Jackalope backends, you can set a couple of +parameters. This section explains the general parameters that are +available with all Jackalope backends. You can also +:ref:`activate logging and profiling `. + +``jackalope.factory`` +..................... + +**type**: ``string or object`` **default**: ``Jackalope\Factory`` + +Use a custom factory class for Jackalope objects. + +``jackalope.check_login_on_server`` +................................... + +**type**: ``boolean`` **default**: ``false`` + +If set to ``false``, skip initial check whether repository exists. You will +only notice connectivity problems on the first attempt to use the repository. + +``jackalope.disable_stream_wrapper`` +.................................... + +**type**: ``boolean`` **default**: ``false`` + +If set to ``true``, streams are read immediately instead of on first access. +If you run into problems with streams this might be useful for debugging. +Otherwise you probably don't want to disable the wrappers, or all binaries +will be loaded each time their containing document is loaded, resulting in a +severe performance penalty. + +``jackalope.auto_lastmodified`` +............................... + +**type**: ``boolean`` **default**: ``true`` + +Whether to automatically update nodes having ``mix:lastModified``. +See `last modified listener cookbook entry`_. + +``backend curl_options`` +"""""""""""""""""""""""" + +If you are using one of the Jackalope Jackrabbit backend, you can set +the curl options which are described in the php-documentation +`curl-setopt`_. + +PHPCR Session with Jackalope Jackrabbit +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. configuration-block:: + + .. code-block:: yaml + + # app/config/config.yml + doctrine_phpcr: + session: + backend: + type: jackrabbit + url: http://localhost:8080/server/ + parameters: + # general parameters and options + # ... + # optional parameters specific to Jackalope Jackrabbit + jackalope.default_header: "X-ID: %serverid%" + jackalope.jackrabbit_expect: true + jackalope.jackrabbit_version: "2.18.3" + + .. code-block:: xml + + + + + + + + + + + X-ID: %serverid% + true + 2.18.3 + + + + + + .. code-block:: php + + // app/config/config.php + $container->loadFromExtension('doctrine_phpcr', [ + 'session' => [ + 'backend' => [ + 'type' => 'jackrabbit', + 'url' => 'http://localhost:8080/server/', + 'parameters' => [ + 'jackalope.default_header' => 'X-ID: %serverid%', + 'jackalope.jackrabbit_expect' => true, + 'jackalope.jackrabbit_version' => '2.18.3', + ], + ], + ], + ]); + +``url`` +""""""" + +**type**: ``string``, **required** + +The configuration needs the ``url`` parameter to point to your Jackrabbit. +This looks like http://localhost:8080/server/ + +``jackalope.default_header`` +"""""""""""""""""""""""""""" + +**type**: ``string``, **default**: ``null`` + +Set a default header to send on each request to the backend. +This is useful when using a load balancer between the webserver and jackrabbit, +to identify sessions. + +``jackalope.jackrabbit_expect`` +""""""""""""""""""""""""""""""" + +**type**: ``boolean``, **default**: ``false`` + +Send the ``Expect: 100-continue`` header on larger PUT and POST requests. +Disabled by default to avoid issues with proxies and load balancers. + +``jackalope.jackrabbit_version`` +"""""""""""""""""""""""""""" + +**type**: ``string``, **default**: ``null`` + +.. versionadded:: 1.4.2 + + This configuration has been added in jackalope-jackrabbit version 1.4.2. + +Set the version of the Jackrabbit server to allow the client to offer better functionality if possible. +For example, full UTF8 support including emojis in node and property names is possible when the Jackrabbit version is 2.18.0 or better. + +PHPCR Session with Jackalope Doctrine DBAL +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +This type uses Jackalope with a Doctrine database abstraction layer transport +to provide PHPCR without any installation requirements beyond any of the RDBMS +supported by Doctrine. + +.. configuration-block:: + + .. code-block:: yaml + + # app/config/config.yml + doctrine_phpcr: + session: + backend: + type: doctrinedbal + # if no explicit connection is specified, the default connection is used. + connection: default + # to configure caching + caches: + meta: doctrine_cache.providers.phpcr_meta + nodes: doctrine_cache.providers.phpcr_nodes + query: doctrine_cache.providers.phpcr_query + parameters: + # ... general parameters and options + + # optional parameters specific to Jackalope Doctrine Dbal + jackalope.disable_transactions: false + + .. code-block:: xml + + + + + + + + + + + + + + + + false + + + + + + .. code-block:: php + + // app/config/config.php + $container->loadFromExtension('doctrine_phpcr', [ + 'session' => [ + 'backend' => [ + 'type' => 'doctrinedbal', + 'connection' => 'default', + 'caches' => [ + 'meta' => 'doctrine_cache.providers.phpcr_meta', + 'nodes' => 'doctrine_cache.providers.phpcr_nodes', + 'query' => 'doctrine_cache.providers.phpcr_query', + ], + 'parameters' => [ + // ... general parameters and options + + // optional parameters specific to Jackalope Doctrine Dbal + 'jackalope.disable_transactions' => false, + ], + ], + ], + ]); + +``connection`` +"""""""""""""" + +**type**: ``string``, **default**: ``default`` + +Specify the Doctrine DBAL connection name to use if you don't want to use the +default connection. The name must be one of the names of the ``doctrine.dbal`` +section of your Doctrine configuration, see the `Symfony Doctrine documentation`_. + +``jackalope.disable_transactions`` +"""""""""""""""""""""""""""""""""" + +**type**: ``boolean``, **default**: ``false`` + +Set to ``true`` to disable transactions. If transactions are enabled but not +actively used, every save operation is wrapped into a transaction. + +Only allowed for doctrine-dbal because jackrabbit does not support +transactions. + +.. _reference-configuration-phpcr-odm-logging: + +Logging and Profiling +~~~~~~~~~~~~~~~~~~~~~ + +When using any of the Jackalope PHPCR implementations, you can activate logging +to log to the symfony log, or profiling to show information in the Symfony +debug toolbar: + +.. configuration-block:: + + .. code-block:: yaml + + # app/config/config.yml + doctrine_phpcr: + session: + backend: + # ... + logging: true + profiling: true + + .. code-block:: xml + + + + + + + + + + + + + + + .. code-block:: php + + // app/config/config.yml + $container->loadFromExtension('doctrine_phpcr', [ + 'session' => [ + 'backend' => [ + // ... + 'logging' => true, + 'profiling' => true, + ], + ], + ]); + +Doctrine PHPCR-ODM Configuration +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +This configuration section manages the Doctrine PHPCR-ODM system. If you do +not configure anything here, the ODM services will not be loaded. + +.. tip:: + + If you want to only use plain PHPCR without the PHPCR-ODM, you can simply + not configure the ``odm`` section to avoid loading the services at all. + Note that most CMF bundles by default use PHPCR-ODM documents and thus + need ODM enabled. + +.. tip:: + + You can also configure multiple document managers. See + :doc:`multiple_sessions` for details. + +.. configuration-block:: + + .. code-block:: yaml + + # app/config/config.yml + doctrine_phpcr: + odm: + configuration_id: ~ + auto_mapping: true + mappings: + # An array of mapping, which may be a bundle name or an unique name + : + mapping: true + type: ~ + dir: ~ + alias: ~ + prefix: ~ + is_bundle: ~ + auto_generate_proxy_classes: "%kernel.debug%" + proxy_dir: "%kernel.cache_dir%/doctrine/PHPCRProxies" + proxy_namespace: PHPCRProxies + namespaces: + translation: + alias: phpcr_locale + + metadata_cache_driver: + type: array + host: ~ + port: ~ + instance_class: ~ + class: ~ + id: ~ + namespace: ~ + + + .. code-block:: xml + + + + + + + + + + + + + mapping="true" + type="null" + dir="null" + alias="null" + prefix="null" + is-bundle="null" + /> + + + + + + + .. code-block:: php + + // app/config/config.php + $container->loadFromExtension('doctrine_phpcr', [ + 'odm' => [ + 'configuration_id' => null, + 'auto_mapping' => true, + 'auto_generate_proxy_classes' => '%kernel.debug%', + 'proxy-dir' => '%kernel.cache_dir%/doctrine/PHPCRProxies', + 'proxy_namespace' => 'PHPCRProxies', + 'namespaces' => [ + 'translation' => [ + 'alias' => 'phpcr_locale', + ], + ], + 'mappings' => [ + // An array of mapping, which may be a bundle name or an unique name + '' => [ + 'mapping' => true, + 'type' => null, + 'dir' => null, + 'alias' => null, + 'prefix' => null, + 'is-bundle' => null, + ], + ], + 'metadata_cache_driver' => [ + 'type' => 'array', + 'host' => null, + 'port' => null, + 'instance_class' => null, + 'class' => null, + 'id' => null, + 'namespace' => null, + ], + ], + ]); + +``configuration_id`` +"""""""""""""""""""" + +**type**: ``string``, **default**: ``doctrine_phpcr.odm.configuration`` + +The service to use as base for building the PHPCR-ODM configuration. + +``auto_mapping`` +"""""""""""""""" + +**type**: ``boolean``, **default**: ``true`` + +When enabled, bundles will be automatically loaded and attempted to resolve +mappings by convention in +``/Resources/config/doctrine/.phpcr.xml`` resp. ``*.phpcr.yml`` +to configure mappings for documents you provide in the ``/Document`` +folder. Otherwise you need to manually configure the mappings section. + +``auto_generate_proxy_classes`` +""""""""""""""""""""""""""""""" + +**type**: ``boolean``, **default**: ``%kernel.debug%`` + +When disabled, you need to run the ``cache:warmup`` command in order to have +the proxy classes generated after you modified a document. + +``proxy_dir`` +""""""""""""" + +**type**: ``string``, **default**: ``%kernel.cache_dir%/doctrine/PHPCRProxies`` + +Change folder where proxy classes are generated. + +``proxy_namespace`` +""""""""""""""""""" + +**type**: ``string``, **default**: ``PHPCRProxies`` + +Change namespace for generated proxy classes. + +``namespaces`` +"""""""""""""" + +This configuration section is intended to allow you to customize the +PHPCR namespaces used by PHPCR-ODM. Currently it is only possible to +set the alias used by the translation strategy. + +``mappings`` +"""""""""""" + +Explicitly define document mappings by configuration. For modern Symfony +applications that do not use a bundle, it is necessary to configure mappings. +For bundles, if ``auto_mapping`` is enabled, you don't usually need to. + +.. tip:: + + When ``auto_mapping`` is disabled, you need to explicitly list the + bundles handled by this document manager. Usually its fine to just list + the bundle names without any actual configuration. + +.. tip:: + + DoctrinePhpcrBundle is integrated with symfony/doctrine-bridge (in the same + way that `Doctrine ORM`_ does), relying on the bridge to process mapping + configuration options. Therefore, the mapping options work nearly the same + across two bundles. + +There are several configuration options that you can control as part of +a mapping definition: + +``mapping`` + A boolean value and it is usually ``true``. Set it to ``true`` to + declare it as a mapping and allow the document manager to pick it + up. + +``type`` + One of ``attribute``, ``xml``, ``yml``, ``php`` or ``staticphp``. + This specifies which type of metadata type your mapping uses. + +``dir`` + Path to the mapping or document files (depending on the driver). If this path + is relative, it is assumed to be relative to the bundle root. This only works + if the name of your mapping is a bundle name. If you want to use this option + to specify absolute paths, you should prefix the path with the kernel + parameters that exist in the DIC (for example ``%kernel.root_dir%``). + +``prefix`` + A common namespace prefix that all documents of this mapping share. This + prefix should never conflict with prefixes of other defined mappings + otherwise some of your documents cannot be found by Doctrine. This option + defaults to the bundle namespace + ``Document``, for example for an + application bundle called ``AcmeHelloBundle`` prefix would be + ``Acme\HelloBundle\Document``. + +``alias`` + Doctrine offers a way to alias document namespaces to simpler, shorter names + to be used in DQL queries or for Repository access. When using a bundle, the + alias defaults to the bundle name. + +``is_bundle`` + This option is a derived value from ``dir`` and by default is set to true if + dir is relative proved by a ``file_exists()`` check that returns false. It + is false if the existence check returns true. In this case, an absolute path + was specified and the metadata files are most likely in a directory outside + of a bundle. + + +``metadata_cache_driver`` +""""""""""""""""""""""""" + +Configure a cache driver for the Doctrine metadata. This is the same as for +`Doctrine ORM`_. + +The ``namespace`` value is useful if you are using one primary caching server +for multiple sites that have similar code in their respective ``vendor/`` +directories. By default, Symfony will try to generate a unique namespace +value for each application but if code is very similar between two +applications, it is very easy to have two applications share the same +namespace. This option also prevents Symfony from needing to re-build +application cache on each Composer update on a newly generated namespace. + +General Settings +~~~~~~~~~~~~~~~~ + +.. configuration-block:: + + .. code-block:: yaml + + # app/config/config.yml + doctrine_phpcr: + jackrabbit_jar: /path/to/jackrabbit.jar + dump_max_line_length: 120 + + .. code-block:: xml + + + + + + + + + .. code-block:: php + + // app/config/config.php + $container->loadFromExtension('doctrine_phpcr', [ + 'jackrabbit_jar' => '/path/to/jackrabbit.jar', + 'dump_max_line_length' => 120, + ]); + +``jackrabbit_jar`` +"""""""""""""""""" + +**type**: ``string`` **default**: ``null`` + +Absolute path to the jackrabbit jar file. If this is set, you can use the +``doctrine:phpcr:jackrabbit`` console command to start and stop Jackrabbit. + +``dump_max_line_length`` +"""""""""""""""""""""""" + +**type**: ``integer`` **default**: ``120`` + +For tuning the output of the ``doctrine:phpcr:dump`` command. + +.. _`Symfony Doctrine documentation`: https://symfony.com/doc/current/doctrine.html +.. _`last modified listener cookbook entry`: http://docs.doctrine-project.org/projects/doctrine-phpcr-odm/en/latest/cookbook/last-modified.html +.. _`Doctrine ORM`: https://symfony.com/doc/current/reference/configuration/doctrine.html#caching-drivers +.. _`curl-setopt`: http://php.net/manual/de/function.curl-setopt.php diff --git a/doc/events.rst b/doc/events.rst new file mode 100644 index 0000000..c470fc5 --- /dev/null +++ b/doc/events.rst @@ -0,0 +1,101 @@ +.. index:: + single: Events; DoctrinePHPCRBundle + +Doctrine PHPCR-ODM Events +========================= + +Doctrine PHPCR-ODM provides an event system allowing to react to all +important operations that documents have during their lifecycle. Please +see the `Doctrine PHPCR-ODM event system documentation`_ for a full +list of supported events. + +The DoctrinePHPCRBundle provides dependency injection support for the +event listeners and event subscribers. + +Dependency Injection Tags +------------------------- + +You can tag services to listen to Doctrine PHPCR-ODM events. It works the same +way as for `Doctrine ORM events`_. The only differences are: + +* use the tag name ``doctrine_phpcr.event_listener`` resp. + ``doctrine_phpcr.event_subscriber`` instead of ``doctrine.event_listener``; +* expect the argument to be of class + ``Doctrine\Common\Persistence\Event\LifecycleEventArgs``. + +To tag a service as event listener and another service as event subscriber, +use this configuration: + +.. configuration-block:: + + .. code-block:: yaml + + # app/config/services.yml + services: + app.phpcr_search_indexer: + class: App\EventListener\SearchIndexer + tags: + - { name: doctrine_phpcr.event_listener, event: postPersist } + + app.phpcr_listener: + class: App\EventListener\MyListener + tags: + - { name: doctrine_phpcr.event_subscriber } + + .. code-block:: xml + + + + + + + + + + + + + + + .. code-block:: php + + // app/config/config.php + use App\EventListener\SearchIndexer; + use App\EventListener\MyListener; + + $container + ->register( + 'app.phpcr_search_indexer', + SearchIndexer::class + ) + ->addTag('doctrine_phpcr.event_listener', [ + 'event' => 'postPersist', + ]) + ; + + $container + ->register( + 'app.phpcr_listener', + MySubscriber::class + ) + ->addTag('doctrine_phpcr.event_subscriber') + ; + +.. tip:: + + Doctrine event subscribers (both ORM and PHPCR-ODM) can **not** return a + flexible array of methods to call like the `Symfony event subscriber`_. + Doctrine event subscribers must return a simple array of the event + names they subscribe to. Doctrine will then expect methods on the + subscriber with the names of the subscribed events, just as when using an + event listener. + +You can find more information and examples of the doctrine event system +in "`How to Register Event Listeners and Subscribers`_" of the core documentation. + +.. _`Doctrine PHPCR-ODM event system documentation`: http://docs.doctrine-project.org/projects/doctrine-phpcr-odm/en/latest/reference/events.html +.. _`Symfony event subscriber`: https://symfony.com/doc/current/components/event_dispatcher/introduction.html#using-event-subscribers +.. _`Doctrine ORM events`: https://symfony.com/doc/current/doctrine/event_listeners_subscribers.html +.. _`How to Register Event Listeners and Subscribers`: https://symfony.com/doc/current/doctrine/event_listeners_subscribers.html diff --git a/doc/fixtures_initializers.rst b/doc/fixtures_initializers.rst new file mode 100644 index 0000000..a55b721 --- /dev/null +++ b/doc/fixtures_initializers.rst @@ -0,0 +1,306 @@ +.. index:: + single: Initializers; DoctrinePHPCRBundle + single: Fixtures; DoctrinePHPCRBundle + single: Migrators; DoctrinePHPCRBundle + +Maintaining Data in the Repository +================================== + +PHPCR-ODM provides *initializers* that ensure a repository is ready for +production use, *migrators* to programmatically load data and +*fixture loading* for handling testing and demo fixtures. + +.. _phpcr-odm-repository-initializers: + +Repository Initializers +----------------------- + +The Initializer is the PHPCR equivalent of the ORM schema tools. It is used to +let your application PHPCR node types and to create required base paths in the +repository. + +.. note:: + + The concept of base paths is needed because there are no separate "tables" + as in a relational database, but one tree containing all data. To be able + to add a document, you need to ensure the parent path is already present + in the repository. + +Initializers have to implement the +``Doctrine\Bundle\PHPCRBundle\Initializer\InitializerInterface``. If you don't +need any special logic and want to create plain PHPCR nodes and not documents, +you can simply define services with ``GenericInitializer``. The generic +Initializer expects a name to identify the Initializer, an array of repository +paths it will create if they do not exist and an optional string defining +namespaces and primary / mixin node types in the CND language that should be +registered with the repository. + +A service to use the generic Initializer looks like this: + +.. configuration-block:: + + .. code-block:: yaml + + # app/config/services.yml + app.phpcr_initializer: + class: Doctrine\Bundle\PHPCRBundle\Initializer\GenericInitializer + arguments: + - App Basepaths + - ["/my/content", "/my/menu"] + - "%app.cnd%" + tags: + - { name: "doctrine_phpcr.initializer" } + + .. code-block:: xml + + + + App Basepaths + + /my/content + /my/menu + + %app.cnd% + + + + .. code-block:: php + + // app/config/services.php + use Doctrine\Bundle\PHPCRBundle\Initializer\GenericInitializer; + use Symfony\Component\DependencyInjection\Definition + + // ... + + $definition = new Definition( + GenericInitializer::class, [ + 'App Basepaths', + ['/my/content', '/my/menu'], + '%app.cnd%', + ] + ); + $definition->addTag('doctrine_phpcr.initializer'); + $container->setDefinition('app.phpcr_initializer', $definition); + +You can execute your Initializers using the following command: + +.. code-block:: bash + + $ php bin/console doctrine:phpcr:repository:init + +.. note:: + The load data fixtures command automatically executes the Initializers + after purging the database, before executing the fixtures. + +The generic Initializer only creates PHPCR nodes. If you want to create +specific documents, you need your own Initializer. The interesting method +to overwrite is the ``init`` method. It is passed the ``ManagerRegistry``, +from which you can retrieve the PHPCR session but also the document manager:: + + // src/App/Initializer/SiteInitializer.php + namespace App\Initializer; + + use App\Documents\Site; + use Doctrine\Bundle\PHPCRBundle\Initializer\InitializerInterface; + use Doctrine\Bundle\PHPCRBundle\ManagerRegistry; + use PHPCR\SessionInterface; + use PHPCR\Util\NodeHelper; + + class SiteInitializer implements InitializerInterface + { + private $basePath; + + public function __construct($basePath = '/cms') + { + $this->basePath = $basePath; + } + + public function init(ManagerRegistry $registry) + { + $dm = $registry->getManagerForClass(Site::class); + if ($dm->find(null, $this->basePath)) { + return; + } + + $site = new Site(); + $site->setId($this->basePath); + $dm->persist($site); + $dm->flush(); + + $session = $registry->getConnection(); + // create the 'cms', 'pages', and 'posts' nodes + NodeHelper::createPath($session, '/cms/pages'); + NodeHelper::createPath($session, '/cms/posts'); + NodeHelper::createPath($session, '/cms/routes'); + + $session->save(); + } + + public function getName() + { + return 'Site Initializer'; + } + } + +Define a service for your Initializer as follows: + +.. configuration-block:: + + .. code-block:: yaml + + # app/config/config.yml + services: + # ... + app.phpcr_initializer_site: + class: App\Initializer\SiteInitializer + tags: + - { name: doctrine_phpcr.initializer } + + .. code-block:: xml + + + + + + + + + + + + .. code-block:: php + + // app/config/config.php + + // ... + $container + ->register( + 'app.phpcr_initializer_site', + 'App\Initializer\SiteInitializer' + ) + ->addTag('doctrine_phpcr.initializer', ['name' => 'doctrine_phpcr.initializer'] + ; + +Migration Loading +----------------- + +The DoctrinePHPCRBundle also ships with a simple command to run migration +scripts. Migrations should implement the +``Doctrine\Bundle\PHPCRBundle\Migrator\MigratorInterface`` and registered as a +service with a ``doctrine_phpcr.migrator`` tag contains an ``alias`` attribute +uniquely identifying the migrator. There is an optional +``Doctrine\Bundle\PHPCRBundle\Migrator\AbstractMigrator`` class to use as a +basis. + +.. configuration-block:: + + .. code-block:: yaml + + # app/config/services.yml + app.migration: + class: App\Migration\Migration + arguments: + - { "%app.content_basepath%", "%app.menu_basepath%" } + tags: + - { name: "doctrine_phpcr.migrator", alias: "app.migration" } + + .. code-block:: xml + + + + + + + %app.content_basepath% + %app.menu_basepath% + + + + + + + .. code-block:: php + + use App\Migration\Migration; + use Symfony\Component\DependencyInjection\Definition; + + // ... + $definition = new Definition(Migration::class, [ + [ + '%app.content_basepath%', + '%app.menu_basepath%', + ], + ]); + $definition->addTag('doctrine_phpcr.migrator', ['alias' => 'app.migration']); + + $container->setDefinition('app.migration', $definition); + +To find out available migrations run: + +.. code-block:: bash + + $ php bin/console doctrine:phpcr:migrator:migrate + +Then pass in the name of the migrator to run it, optionally passing in an +``--identifier``, ``--depth`` or ``--session`` argument. The later argument +determines which session name to set on the migrator, while the first two +arguments will simply be passed to the ``migrate()`` method. You can find an +example migrator in the SimpleCmsBundle. + +.. tip:: + + A simple alternative if you do not need to reproduce the result can be to + export part of your repository and re-import it on the target server. This + is described in :ref:`phpcr-odm-backup-restore`. + +.. _phpcr-odm-repository-fixtures: + +Fixture Loading +--------------- + +To use the ``doctrine:phpcr:fixtures:load`` command, you additionally need to +install the `DoctrineFixturesBundle`_ which brings the +`Doctrine data-fixtures`_ into Symfony. + +Fixtures work the same way they work for Doctrine ORM. You write fixture +classes implementing ``Doctrine\Common\DataFixtures\FixtureInterface``. If you +place them in ``\DataFixtures\PHPCR``, they will be auto detected if you +don't specify a path in the command. + +A simple example fixture class looks like this:: + + // src/App/DataFixtures/PHPCR/LoadPageData.php + namespace App\DataFixtures\PHPCR; + + use Doctrine\Common\Persistence\ObjectManager; + use Doctrine\Common\DataFixtures\FixtureInterface; + use Doctrine\ODM\PHPCR\DocumentManager; + + class LoadPageData implements FixtureInterface + { + public function load(ObjectManager $manager) + { + if (!$manager instanceof DocumentManager) { + $class = get_class($manager); + throw new \RuntimeException("Fixture requires a PHPCR ODM DocumentManager instance, instance of '$class' given."); + } + + // ... create and persist your data here + } + } + +For more on fixtures, see the documentation of the `DoctrineFixturesBundle`_. + +.. _`DoctrineFixturesBundle`: https://symfony.com/doc/current/bundles/DoctrineFixturesBundle/index.html +.. _`Doctrine data-fixtures`: https://github.com/doctrine/data-fixtures diff --git a/doc/forms.rst b/doc/forms.rst new file mode 100644 index 0000000..eb1bd34 --- /dev/null +++ b/doc/forms.rst @@ -0,0 +1,183 @@ +.. index:: + single: Form Types; DoctrinePHPCRBundle + +Doctrine PHPCR-ODM Form Types +============================= + +This bundle provides some handy form types for PHPCR and PHPCR-ODM specific +cases, along with a type guesser that uses these types. + +There is also a validator constraint for PHPCR-ODM documents. + +Form Types +---------- + +.. tip:: + + When editing associative multivalue fields, have a look at the + BurgovKeyValueFormBundle_. + +phpcr_document +~~~~~~~~~~~~~~ + +This form type is suitable to edit associations of PHPCR-ODM documents. It +works for ReferenceOne, ReferenceMany and Referrers but also for +ParentDocument associations. Make sure to set the ``multiple`` option +for ReferenceMany and Referrers, and to not set it for the others. + +.. note:: + + While ``Children`` is also an association, it makes no sense to edit it + with this form type. Children are automatically attached to their parent. + ``MixedReferrers`` could be shown as a ``disabled`` field but never edited, + because this association is immutable. + +This form type is equivalent to the ``entity`` form type provided by Symfony +for Doctrine ORM. It has the same options as the ``entity`` type, including +that the option for the document manager is called ``em``. + +A simple example of using the ``phpcr_document`` form type looks as follows:: + + use App\Document\TargetClass; + + $form + ->add( + 'speakers', + 'phpcr_document', + [ + 'property' => 'title', + 'class' => TargetClass::class, + 'multiple' => true, + ] + ) + ; + +This will produce a multiple choice select field with the value of +``getTitle`` called on each instance of ``TargetClass`` found in the +content repository. Alternatively, you can set the ``choices`` option +to a list of allowed managed documents. Please refer to the +`Symfony documentation on the entity form type`_ for more details, +including how you can configure a query. + +If you are using SonataDoctrinePHPCRAdminBundle_, you might want to look into +``sonata_type_collection``. That form type allows to edit related +documents (references as well as children) in-line and also to create +and remove them on the fly. + +phpcr_odm_reference_collection +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. caution:: + + This form type was deprecated in DoctrinePHPCRBundle 1.1 and will be + removed in DoctrinePHPCRBundle 1.2. You should use the `phpcr_document`_ + type instead, which can do the same but better. + +This form type handles editing ``ReferenceMany`` collections on PHPCR-ODM +documents. It is a choice field with an added ``referenced_class`` required +option that specifies the class of the referenced target document. + +To use this form type, you also need to specify the list of possible reference +targets as an array of PHPCR-ODM ids or PHPCR paths. + +The minimal code required to use this type looks as follows:: + + use App\Document\Article; + + $dataArr = [ + '/some/phpcr/path/item_1' => 'first item', + '/some/phpcr/path/item_2' => 'second item', + ]; + + $formMapper + ->with('form.group_general') + ->add('myCollection', 'phpcr_odm_reference_collection', [ + 'choices' => $dataArr, + 'referenced_class' => Article::class, + ]) + ->end(); + +.. tip:: + + When building an admin interface with the SonataDoctrinePHPCRAdminBundle_ + there is also the ``sonata_type_model``, which is more powerful, allowing to + add to the referenced documents on the fly. + +phpcr_reference +~~~~~~~~~~~~~~~ + +The ``phpcr_reference`` represents a PHPCR Property of type REFERENCE or +WEAKREFERENCE within a form. The input will be rendered as a text field +containing either the PATH or the UUID as per the configuration. The form will +resolve the path or id back to a PHPCR node to set the reference. + +This type extends the ``text`` form type. It adds an option +``transformer_type`` that can be set to either ``path`` or ``uuid``. + + +Validator Constraint +-------------------- + +The bundle provides a ``ValidPhpcrOdm`` constraint validator you can use to +check if your document ``Id`` or ``Nodename`` and ``Parent`` fields are +correct. + +.. configuration-block:: + + .. code-block:: yaml + + # src/App/Resources/config/validation.yml + App\Document\Author: + constraints: + - Doctrine\Bundle\PHPCRBundle\Validator\Constraints\ValidPhpcrOdm + + .. code-block:: php + + // src/App/Document/Author.php + + // ... + use Doctrine\Bundle\PHPCRBundle\Validator\Constraints as OdmAssert; + + #[OdmAssert\ValidPhpcrOdm] + class Author + { + // ... + } + + .. code-block:: xml + + + + + + + + + + + + .. code-block:: php + + // src/App/Document/Author.php + + // ... + use Symfony\Component\Validator\Mapping\ClassMetadata; + use Doctrine\Bundle\PHPCRBundle\Validator\Constraints as OdmAssert; + + #[OdmAssert\ValidPhpcrOdm] + class Author + { + // ... + + public static function loadValidatorMetadata(ClassMetadata $metadata) + { + $metadata->addConstraint(new OdmAssert\ValidPhpcrOdm()); + } + } + +.. _BurgovKeyValueFormBundle: https://github.com/Burgov/KeyValueFormBundle +.. _`Symfony documentation on the entity form type`: https://symfony.com/doc/current/reference/forms/types/entity.html +.. _SonataDoctrinePHPCRAdminBundle: https://sonata-project.org/bundles/doctrine-phpcr-admin/master/doc/index.html diff --git a/doc/index.rst b/doc/index.rst new file mode 100644 index 0000000..bd00e80 --- /dev/null +++ b/doc/index.rst @@ -0,0 +1,14 @@ +DoctrinePHPCRBundle +=================== + +.. toctree:: + :maxdepth: 2 + + introduction + models + events + forms + fixtures_initializers + multilang + multiple_sessions + configuration diff --git a/doc/introduction.rst b/doc/introduction.rst new file mode 100644 index 0000000..92847cb --- /dev/null +++ b/doc/introduction.rst @@ -0,0 +1,568 @@ +.. index:: + single: PHPCR; Bundles + single: DoctrinePHPCRBundle + +DoctrinePHPCRBundle +=================== + +The `DoctrinePHPCRBundle`_ provides integration with the PHP content +repository and optionally with Doctrine PHPCR-ODM to provide the ODM document +manager in symfony. + +Out of the box, this bundle supports the following PHPCR implementations: + +* `Jackalope`_ (Jackrabbit, Doctrine DBAL and prismic transports) + +.. tip:: + + This reference only explains the Symfony integration of PHPCR and + PHPCR-ODM. To learn how to use PHPCR, refer to `the PHPCR website`_ and + for Doctrine PHPCR-ODM to the `PHPCR-ODM documentation`_. + +Setup +----- + +Requirements +~~~~~~~~~~~~ + +* When using **jackalope-jackrabbit**: Java, Apache Jackalope and ``libxml`` + version >= 2.7.0 (due to a `bug in libxml`_) +* When using **jackalope-doctrine-dbal with MySQL**: MySQL >= 5.1.5 + (as you need the xml function ``ExtractValue``) + +Installation +------------ + +You can install this bundle `with composer`_ using the +`doctrine/phpcr-bundle`_ package. You need a concrete implementation of +the PHPCR API. For this example, we assume that you require Jackalope Doctrine +DBAL. See the `PHPCR-ODM documentation` for alternatives. + +If you want to use PHPCR-ODM, you additionally need to require +``doctrine/phpcr-odm``. + +.. code-block:: javascript + + require: { + ... + "jackalope/jackalope-doctrine-dbal": "1.2.*", + "doctrine/phpcr-odm": "1.2.*", + "doctrine/phpcr-bundle": "1.2.*", + ... + } + +Besides the ``DoctrinePHPCRBundle`` you also need to instantiate the base +``DoctrineBundle`` in your kernel:: + + // app/AppKernel.php + + // ... + class AppKernel extends Kernel + { + public function registerBundles() + { + $bundles = [ + // ... + new Doctrine\Bundle\PHPCRBundle\DoctrinePHPCRBundle(), + new Doctrine\Bundle\DoctrineBundle\DoctrineBundle(), + ]; + + // ... + } + + // ... + } + +Configuration +------------- + +PHPCR Session Configuration +~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The session needs a PHPCR implementation specified in the ``backend`` section +by the ``type`` field, along with configuration options to bootstrap the +implementation. The examples here assume that you are using Jackalope Doctrine +DBAL. The full documentation is in the :doc:`configuration reference `. + +To use Jackalope Doctrine DBAL, you need to configure a database connection +with the DoctrineBundle. For detailed information, see the +`Symfony Doctrine documentation`_. A simple example is: + +.. code-block:: yaml + + # app/config/parameters.yml + parameters: + database_driver: pdo_mysql + database_host: localhost + database_name: test_project + database_user: root + database_password: password + + # ... + +.. configuration-block:: + + .. code-block:: yaml + + # app/config/config.yml + doctrine: + dbal: + driver: "%database_driver%" + host: "%database_host%" + dbname: "%database_name%" + user: "%database_user%" + password: "%database_password%" + + .. code-block:: xml + + + + + + + + + + + + .. code-block:: php + + // app/config/config.php + $configuration->loadFromExtension('doctrine', [ + 'dbal' => [ + 'driver' => '%database_driver%', + 'host' => '%database_host%', + 'dbname' => '%database_name%', + 'user' => '%database_user%', + 'password' => '%database_password%', + ], + ]); + +Jackalope Doctrine DBAL provides a PHPCR implementation without any +installation requirements beyond any of the RDBMS supported by Doctrine. +Once you set up Doctrine DBAL, you can configure Jackalope: + +.. configuration-block:: + + .. code-block:: yaml + + # app/config/config.yml + doctrine_phpcr: + session: + backend: + type: doctrinedbal + # connection: default + + # requires DoctrineCacheBundle + # caches: + # meta: doctrine_cache.providers.phpcr_meta + # nodes: doctrine_cache.providers.phpcr_nodes + # enable logging + logging: true + # enable profiling in the debug toolbar. + profiling: true + workspace: default + username: admin + password: admin + + .. code-block:: xml + + + + + + + + + + + + + + + + + + .. code-block:: php + + // app/config/config.php + $container->loadFromExtension('doctrine_phpcr', [ + 'session' => [ + 'backend' => [ + 'type' => 'doctrinedbal', + //'connection': 'default', + 'logging' => true, + 'profiling' => true, + //'caches' => [ + // 'meta' => 'doctrine_cache.providers.phpcr_meta' + // 'nodes' => 'doctrine_cache.providers.phpcr_nodes' + //], + ], + 'workspace' => 'default', + 'username' => 'admin', + 'password' => 'admin', + ], + ]); + +Now make sure the database exists and initialize it: + +.. code-block:: bash + + # without Doctrine ORM + php bin/console doctrine:database:create + php bin/console doctrine:phpcr:init:dbal + +.. tip:: + + You can also use a different doctrine dbal connection instead of the + default. Specify the dbal connection name in the ``connection`` option of + the ``backend`` configuration. + + It is recommended to use a separate connection to a separate database if + you also use Doctrine ORM or direct DBAL access to data, rather than + mixing this data with the tables generated by Jackalope Doctrine Dbal. If + you have a separate connection, you need to pass the alternate connection + name to the ``doctrine:database:create`` command with the ``--connection`` + option. For Doctrine PHPCR commands, this parameter is not needed as you + configured the connection to use. + +If you are using Doctrine ORM on the same connection, the schema is integrated +into ``doctrine:schema:create|update|drop`` and also `DoctrineMigrationsBundle`_ +so that you can create migrations. + +.. code-block:: bash + + # Using Doctrine ORM + php bin/console doctrine:database:create + php bin/console doctrine:schema:create + +.. note:: + + To use the cache, install and configure the DoctrineCacheBundle and uncomment + the cache meta and nodes settings. + +Doctrine PHPCR-ODM Configuration +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +This configuration section manages the document mapper system that converts +your PHPCR nodes to domain model objects. If you do not configure anything +here, the ODM services will not be loaded. + +.. configuration-block:: + + .. code-block:: yaml + + # app/config/config.yml + doctrine_phpcr: + odm: + auto_mapping: true + auto_generate_proxy_classes: "%kernel.debug%" + mappings: + App: + mapping: true + type: attribute + dir: '%kernel.root_dir%/Document' + alias: App + prefix: App\Document\ + is_bundle: false + + .. code-block:: xml + + + + + + + + + + + + + + .. code-block:: php + + // app/config/config.php + $container->loadFromExtension('doctrine_phpcr', [ + 'odm' => [ + 'auto_mapping' => true, + 'auto_generate_proxy_classes' => '%kernel.debug%', + 'mappings' => [ + # Configure document mappings + 'App' => [ + 'mapping' => true, + 'type' => 'attribute', + 'dir' => '%kernel.root_dir%/Document', + 'alias' => 'App', + 'prefix' => 'App\Document\', + 'is-bundle' => false, + ], + ], + ], + ]); + +When ``auto_mapping`` is enabled, bundles will be automatically loaded and +attempted to resolve mappings + +.. tip:: + + For bundles, unless you disable ``auto_mapping``, you can place your + documents in the ``Document`` folder inside your bundles and use + attribute or name the mapping files following this convention: + ``/Resources/config/doctrine/.phpcr.xml`` or + ``*.phpcr.yml``. + +If ``auto_generate_proxy_classes`` is false, you need to run the +``cache:warmup`` command in order to have the proxy classes generated after +you modified a document. This is usually done in production to gain some performance. + +For applications, it is usually required to define ``mappings``. In a standard +minimal setup, an ``App`` definition as shown in above example is required, +which maps ``App\Document\`` documents in the ``src/Document`` directory. + +See :doc:`configuration`, for complete details. + + +Registering System Node Types +""""""""""""""""""""""""""""" + +PHPCR-ODM uses a `custom node type`_ to track meta information without +interfering with your content. There is a command that makes it trivial to +register this type and the PHPCR namespace, as well as all base paths of +bundles: + +.. code-block:: bash + + $ php bin/console doctrine:phpcr:repository:init + +You only need to run this command once when you created a new repository. (But +nothing goes wrong if you run it on each deployment for example.) + +Profiling and Performance of Jackalope +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +When using any of the Jackalope PHPCR implementations, you can activate logging +to log to the symfony log, or profiling to show information in the Symfony +debug toolbar: + +.. configuration-block:: + + .. code-block:: yaml + + # app/config/config.yml + doctrine_phpcr: + session: + backend: + # ... + logging: true + profiling: true + + .. code-block:: xml + + + + + + + + + + + + + + + .. code-block:: php + + // app/config/config.yml + $container->loadFromExtension('doctrine_phpcr', [ + 'session' => [ + 'backend' => [ + // ... + 'logging' => true, + 'profiling' => true, + ], + ], + ]); + +Now that you can see the effects of changes, you can try if adjusting the global +fetch depth reduces the number and duration for queries. Set the option +``jackalope.fetch_depth`` to something bigger than 0 to have Jackalope pre-fetch +children or whole subtrees. This can reduce the number of queries needed, but +watch out for longer queries because more data is fetched. + +When using Jackalope Doctrine DBAL, it is highly recommended to activate the +caching options. + +Note that you can also set the fetch-depth on the session on the fly for +specific calls, or use the fetch-depth option on children mappings of your +documents. + +The parameter ``jackalope.check_login_on_server`` can be set to false to save +an initial call to the database to check if the connection works. + +Services +-------- + +There are 3 main services provided by this bundle: + +* ``Doctrine\Bundle\PHPCRBundle\ManagerRegistry``- The ``ManagerRegistry`` + instance with references to all sessions and document manager instances; +* ``PHPCR\SessionInterface`` - the PHPCR session. If you configured + multiple sessions, this will be the default session; +* ``Doctrine\ODM\PHPCR\DocumentManagerInterface`` - the PHPCR-ODM document + manager. If you configured multiple managers, this will be the default + manager. + +.. _bundles-phpcr-odm-commands: + +Doctrine PHPCR Commands +----------------------- + +All commands about PHPCR are prefixed with ``doctrine:phpcr`` and you can use +the --session argument to use a non-default session if you configured several +PHPCR sessions. + +Some of these commands are specific to a backend or to the ODM. Those commands +will only be available if such a backend is configured. + +Use ``php bin/console help `` to see all options each of the commands +has. + +* **doctrine:phpcr:document:migrate-class**: Command to migrate document classes; +* **doctrine:phpcr:fixtures:load**: Load data fixtures to your PHPCR database; +* **doctrine:phpcr:init:dbal**: Prepare the database for Jackalope Doctrine-Dbal; +* **doctrine:phpcr:jackrabbit**: Start and stop the Jackrabbit server; +* **doctrine:phpcr:mapping:info**: Shows basic information about all mapped documents; +* **doctrine:phpcr:migrator:migrate**: Migrates PHPCR data; +* **doctrine:phpcr:node-type:list**: List all available node types in the repository; +* **doctrine:phpcr:node-type:register**: Register node types in the PHPCR repository; +* **doctrine:phpcr:node:dump**: Dump subtrees of the content repository; +* **doctrine:phpcr:node:move**: Moves a node from one path to another; +* **doctrine:phpcr:node:remove**: Remove content from the repository; +* **doctrine:phpcr:node:touch**: Create or modify a node; +* **doctrine:phpcr:nodes:update**: Command to manipulate the nodes in the workspace; +* **doctrine:phpcr:repository:init**: Initialize the PHPCR repository; +* **doctrine:phpcr:workspace:create**: Create a workspace in the configured repository; +* **doctrine:phpcr:workspace:export**: Export nodes from the repository, + either to the JCR system view format or the document view format; +* **doctrine:phpcr:workspace:import**: Import xml data into the repository, + either in JCR system view format or arbitrary xml; +* **doctrine:phpcr:workspace:list**: List all available workspaces in the configured repository; +* **doctrine:phpcr:workspace:purge**: Remove all nodes from a workspace; +* **doctrine:phpcr:workspace:query**: Execute a JCR SQL2 statement. + +.. note:: + + To use the ``doctrine:phpcr:fixtures:load`` command, you additionally need + to install the `DoctrineFixturesBundle`_ and its dependencies. See + :ref:`phpcr-odm-repository-fixtures` for how to use fixtures. + +Some Example Command Runs +~~~~~~~~~~~~~~~~~~~~~~~~~ + +Running `SQL2 queries`_ against the repository: + +.. code-block:: bash + + $ php bin/console doctrine:phpcr:workspace:query "SELECT title FROM [nt:unstructured] WHERE NAME() = 'home'" + +Dumping nodes under ``/cms/simple`` including their properties: + +.. code-block:: bash + + $ php bin/console doctrine:phpcr:node:dump /cms/simple --props + +.. _phpcr-odm-backup-restore: + +Simple Backup and Restore +~~~~~~~~~~~~~~~~~~~~~~~~~ + +To export all repository data into a file, you can use: + +.. code-block:: bash + + $ php bin/console doctrine:phpcr:workspace:export --path /cms /path/to/backup.xml + +.. note:: + + You always want to specify a path to export. Without any path you will + export the root node of the repository, which will be imported later as + ``jcr:root``. + +To restore this backup you can run: + +.. code-block:: bash + + $ php bin/console doctrine:phpcr:workspace:import /path/to/backup.xml + +Note that you can also export and import parts of your repository by choosing a +different path on export and specifying the ``--parentpath`` option to the +import. + +If you already have data in your repository that you want to replace, you can +remove the target node first: + +.. code-block:: bash + + $ php bin/console doctrine:phpcr:node:remove /cms + +Read On +------- + +* :doc:`models` +* :doc:`events` +* :doc:`forms` +* :doc:`fixtures_initializers` +* :doc:`multilang` +* :doc:`multiple_sessions` +* :doc:`configuration` + +.. _`PHPCR-ODM documentation`: https://www.doctrine-project.org/projects/doctrine-phpcr-odm/en/latest/index.html +.. _`DoctrinePHPCRBundle`: https://github.com/doctrine/DoctrinePHPCRBundle +.. _`Symfony Doctrine documentation`: https://symfony.com/doc/current/doctrine.html +.. _`Jackalope`: http://jackalope.github.io/ +.. _`the PHPCR website`: https://phpcr.github.io/ +.. _`bug in libxml`: https://bugs.php.net/bug.php?id=36501 +.. _`with composer`: https://getcomposer.org +.. _`doctrine/phpcr-bundle`: https://packagist.org/packages/doctrine/phpcr-bundle +.. _`custom node type`: https://github.com/doctrine/phpcr-odm/wiki/Custom-node-type-phpcr%3Amanaged +.. _`DoctrineMigrationsBundle`: https://symfony.com/bundles/DoctrineMigrationsBundle/current/index.html +.. _`DoctrineFixturesBundle`: https://symfony.com/bundles/DoctrineFixturesBundle/current/index.html +.. _`SQL2 queries`: http://www.h2database.com/jcr/grammar.html diff --git a/doc/models.rst b/doc/models.rst new file mode 100644 index 0000000..46c245f --- /dev/null +++ b/doc/models.rst @@ -0,0 +1,342 @@ +.. index:: + single: PHPCR; Bundles + single: DoctrinePHPCRBundle + +Modeling Data with PHPCR-ODM +============================ + +The Doctrine PHPCR-ODM is a doctrine object-mapper on top of the +`PHP Content Repository`_ (PHPCR), which is a PHP adaption of the +`JSR-283 specification`_. The most important feature of PHPCR is the tree +structure to store the data. All data is stored in items of a tree, called +nodes. You can think of this like a file system, that makes it perfect to use +in a CMS. + +On top of the tree structure, PHPCR also adds features like searching, +versioning and access control. + +Doctrine PHPCR-ODM has the same API as the other Doctrine libraries, like the +`Doctrine ORM`_. The Doctrine PHPCR-ODM adds another great feature to PHPCR: +multi-language support. + +A Simple Example: A Task +------------------------ + +The easiest way to get started with the PHPCR-ODM is to see it in action. In +this section, you are going to create a ``Task`` object and learn how to +persist it. + +Creating a Document Class +~~~~~~~~~~~~~~~~~~~~~~~~~ + +Without thinking about Doctrine or PHPCR-ODM, you can create a ``Task`` object +in PHP:: + + // src/App/Document/Task.php + namespace use App\Document; + + class Task + { + protected $description; + + protected $done = false; + } + +This class - often called a "document" in PHPCR-ODM, meaning *a basic class +that holds data* - is simple and helps fulfill the business requirement of +needing tasks in your application. This class can't be persisted to +Doctrine PHPCR-ODM yet - it's just a simple PHP class. + +.. note:: + + A Document is analogous to the term ``Entity`` employed by the Doctrine + ORM. To have the mapping happen automatically, place your documents in the + ``Document`` namespace within your application. + +Add Mapping Information +~~~~~~~~~~~~~~~~~~~~~~~ + +Doctrine allows you to work with PHPCR in a much more interesting way than +just fetching data back and forth as an array. Instead, Doctrine allows you to +persist entire objects to PHPCR and fetch entire *objects* out of PHPCR. +This works by mapping a PHP class and its properties to the PHPCR tree. + +For Doctrine to be able to do this, you just have to create "metadata", or +configuration that tells Doctrine exactly how the ``Task`` document and its +properties should be *mapped* to PHPCR. This metadata can be specified in a +number of different formats including YAML, XML or directly inside the ``Task`` +class via attributes: + +.. configuration-block:: + + .. code-block:: php + + // src/App/Document/Task.php + namespace App\Document; + + use Doctrine\ODM\PHPCR\Mapping\Attributes as PHPCR; + + #[PHPCR\Document] + class Task + { + #[PHPCR\Id] + private $id; + + #[PHPCR\Field(type: 'string')] + private $description; + + #[PHPCR\Field(type: 'boolean')] + private $done = false; + + #[PHPCR\ParentDocument] + private $parentDocument; + } + + .. code-block:: yaml + + # src/App/Resources/config/doctrine/Task.phpcr.yml + App\Document\Task: + id: id + + fields: + description: string + done: boolean + + parent_document: parentDocument + + .. code-block:: xml + + + + + + + + + + + + + + + + + +After this, you have to create getters and setters for the properties. + +.. note:: + + This Document uses the parent document and a node name to determine its + position in the tree. Because there isn't any name set, it is generated + automatically. If you want to use a specific node name, such as a + slugified version of the title, you need to add a property mapped as + ``Nodename``. + + A Document must have an id property. This represents the full path (parent + path + name) of the Document. This will be set by Doctrine by default and + it is not recommend to use the id to determine the location of a Document. + + For more information about identifier generation strategies, refer to the + `doctrine documentation`_ + +.. tip:: + + You may want to implement ``Doctrine\ODM\PHPCR\HierarchyInterface`` to + expose the hierarchy in a standardized way. + +.. seealso:: + + You can also check out Doctrine's `Basic Mapping Documentation`_ for all + details about mapping information. If you use attributes, you'll need to + prepend all attributes with ``PHPCR\``, which is the name of the imported + namespace (e.g. ``#[PHPCR\Document(..)]``), this is not shown in Doctrine's + documentation. You'll also need to include the + ``use Doctrine\ODM\PHPCR\Mapping\Attributes as PHPCR;`` statement to + import the PHPCR attributes prefix. + +Persisting Documents to PHPCR +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Now that you have a mapped ``Task`` document, complete with getter and setter +methods, you are ready to persist data to PHPCR. For a simple example, lets do +this from inside a controller:: + + // src/App/Controller/DefaultController.php + + // ... + use App\Document\Task; + use Doctrine\ODM\PHPCR\DocumentManagerInterface; + use Symfony\Component\HttpFoundation\Response; + + // ... + public function createAction(DocumentManagerInterface $documentManager) + { + $rootTask = $documentManager->find(null, '/tasks'); + + $task = new Task(); + $task->setDescription('Finish CMF project'); + $task->setParentDocument($rootTask); + + $documentManager->persist($task); + + $documentManager->flush(); + + return new Response('Created task "'.$task->getDescription().'"'); + } + +Take a look at the previous example in more detail: + +* **line 8** We use symfony controller injection with autowiring to get the + *document manager*. This service is responsible for storing and fetching + objects to and from PHPCR. +* **line 10** This line loads the root document for the tasks, as each PHPCR + document needs to have a parent. To create this root document, you can + configure a :ref:`Repository Initializer `, + which will be executed when running ``doctrine:phpcr:repository:init``. +* **lines 12-14** In this section, you instantiate and work with the ``$task`` + object like any other, normal PHP object. +* **line 16** The ``persist()`` method tells Doctrine to "manage" the ``$task`` + object. This does not actually cause a query to be made to PHPCR (yet). +* **line 20** When the ``flush()`` method is called, Doctrine looks through all + of the objects that it is managing to see if they need to be stored to PHPCR. + In this example, the ``$task`` object has not been saved yet, so the document + manager makes a query to PHPCR to add it. + +When creating or updating objects, the workflow is always the same. In the +next section, you'll see how Doctrine is smart enough to update documents if +they already exist in PHPCR. + +Fetching Objects from PHPCR +~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Fetching an object back out of PHPCR is even easier. For example, suppose +you've configured a route to display a specific task by name:: + + use App\Document\Task; + use Doctrine\ODM\PHPCR\DocumentManagerInterface; + + public function showAction(DocumentManagerInterface $documentManager, $name) + { + $repository = $documentManager->getRepository(Task::class); + $task = $repository->find('/tasks/'.$name); + + if (!$task) { + throw $this->createNotFoundException('No task found with name '.$name); + } + + return new Response('['.($task->isDone() ? 'x' : ' ').'] '.$task->getDescription()); + } + +To retrieve objects from the document repository using both the ``find`` and +``findMany`` methods and all helper methods of a class-specific repository. In +PHPCR, it's often unknown for developers which node has the data for a specific +document, in that case you should use the document manager to find the nodes +(for instance, when you want to get the root document). In example above, we +know they are ``Task`` documents and so we can use the repository. + +The repository contains all sorts of helpful methods:: + + // query by the id (full path) + $task = $repository->find($id); + + // query for one task matching be name and done + $task = $repository->findOneBy(['name' => 'foo', 'done' => false]); + + // query for all tasks matching the name, ordered by done + $tasks = $repository->findBy( + ['name' => 'foo'], + ['done' => 'ASC'] + ); + +.. tip:: + + If you use the repository class, you can also create a custom repository + for a specific document. This helps with "Separation of Concern" when using more + complex queries. This is similar to how it's done in Doctrine ORM, for + more information read "`Custom Repository Classes`_" in the core + documentation. + +.. tip:: + + You can also query objects by using the Query Builder provided by + Doctrine PHPCR-ODM. For more information, read + `the QueryBuilder documentation`_. + +Updating an Object +~~~~~~~~~~~~~~~~~~ + +Once you've fetched an object from Doctrine, updating it is easy. Suppose you +have a route that maps a task ID to an update action in a controller:: + + use App\Document\Task; + use Doctrine\ODM\PHPCR\DocumentManagerInterface; + + public function updateAction(DocumentManagerInterface $documentManager, $name) + { + $repository = $documentManager->getRepository(Task::class); + $task = $repository->find('/tasks/'.$name); + + if (!$task) { + throw $this->createNotFoundException('No task found for name '.$name); + } + + if (!$task->isDone()) { + $task->setDone(true); + } + + $documentManager->flush(); + + return new Response('[x] '.$task->getDescription()); + } + +Updating an object involves just three steps: + +#. fetching the object from Doctrine; +#. modifying the object; +#. calling ``flush()`` on the document manager + +Notice that calling ``$documentManger->persist($task)`` isn't necessary. +Recall that this method simply tells Doctrine to manage or "watch" the +``$task`` object. In this case, since you fetched the ``$task`` object from +Doctrine, it's already managed. + +Deleting an Object +~~~~~~~~~~~~~~~~~~ + +Deleting an object is very similar, but requires a call to the ``remove()`` +method of the document manager after you fetched the document from PHPCR:: + + $documentManager->remove($task); + $documentManager->flush(); + +As you might expect, the ``remove()`` method notifies Doctrine that you'd like +to remove the given document from PHPCR. The actual delete operation +however, is not actually executed until the ``flush()`` method is called. + +Summary +------- + +With Doctrine, you can focus on your objects and how they're useful in your +application and worry about database persistence second. This is because +Doctrine allows you to use any PHP object to hold your data and relies on +mapping metadata information to map an object's data to a particular database +table. + +And even though Doctrine revolves around a simple concept, it's incredibly +powerful, allowing you to `create complex queries`_ and +:doc:`subscribe to events ` that allow you to take different actions as +objects go through their persistence lifecycle. + +.. _`PHP Content Repository`: http://phpcr.github.io/ +.. _`JSR-283 specification`: https://jcp.org/en/jsr/detail?id=283 +.. _`Doctrine ORM`: https://symfony.com/doc/current/doctrine.html +.. _`doctrine documentation`: http://docs.doctrine-project.org/projects/doctrine-phpcr-odm/en/latest/reference/basic-mapping.html#basicmapping-identifier-generation-strategies +.. _`Basic Mapping Documentation`: http://docs.doctrine-project.org/projects/doctrine-phpcr-odm/en/latest/reference/basic-mapping.html +.. _`the QueryBuilder documentation`: http://docs.doctrine-project.org/projects/doctrine-phpcr-odm/en/latest/reference/query-builder.html +.. _`create complex queries`: http://docs.doctrine-project.org/projects/doctrine-phpcr-odm/en/latest/reference/query-builder.html +.. _`Custom Repository Classes`: https://symfony.com/doc/current/doctrine/repository.html diff --git a/doc/multilang.rst b/doc/multilang.rst new file mode 100644 index 0000000..bec5413 --- /dev/null +++ b/doc/multilang.rst @@ -0,0 +1,197 @@ +.. index:: + single: Multi-Language; DoctrinePHPCRBundle + +Doctrine PHPCR-ODM Multi-Language Support +========================================= + +PHPCR-ODM can handle translated documents. All translations of the same +document are considered the same document. Only one language version can be +loaded at the same time. + +Note that the CMF routing component does not use translated routes but has a +`separate route per language`_. + +To use the multi-language features of PHPCR-ODM you need to enable locales in +the configuration. + +Translation Configuration +------------------------- + +To use translated documents, you need to configure the available languages: + +.. configuration-block:: + + .. code-block:: yaml + + # app/config/config.yml + doctrine_phpcr: + odm: + # ... + locales: + en: [de, fr] + de: [en, fr] + fr: [en, de] + locale_fallback: hardcoded + default_locale: fr + + .. code-block:: xml + + + + + + + + + + + de + fr + + + + en + fr + + + + en + de + + + fr + + + + + .. code-block:: php + + // app/config/config.php + $container->loadFromExtension('doctrine_phpcr', [ + 'odm' => [ + // ... + 'locales' => [ + 'en' => ['de', 'fr'], + 'de' => ['en', 'fr'], + 'fr' => ['en', 'de'], + ], + 'locale_fallback' => 'hardcoded', + 'default_locale' => 'fr', + ] + ]); + +The ``locales`` is a list of alternative locales to look up if a document +is not translated to the requested locale. + +The default locale is used for the standard locale chooser strategy and +hence will be the default locale in the document manager. Specifying the +default locale is optional. If you do not specify a default locale then the +first locale listed is used as the default locale. + +This bundle provides a request listener that gets activated when any locales +are configured. This listener updates PHPCR-ODM to use the locale Symfony +determined for this request, if that locale is in the list of keys defined +under ``locales``. + +Fallback strategies +~~~~~~~~~~~~~~~~~~~ + +There are several strategies to adjust the fallback order for the selected +locale based on the accepted languages of the request (determined by Symfony +from the ``Accept-Language`` HTML header). All of them will never add any +locales that where not configured in the ``locales`` to avoid a request +injecting unexpected things into your repository: + +* ``hardcoded``: This strategy does not update the fallback order from + the request; +* ``replace``: takes the accepted locales from the request and updates the + fallback order with them, removing any locales not found in the request; +* ``merge``: does the same as ``replace`` but then adds locales not found in + the request but on the ``locales`` configuration back to the end of the + fallback list. This reorders the locales without losing any of them. This is + the default strategy. + +Translated documents +-------------------- + +To make a document translated, you need to define the ``translator`` attribute +on the document mapping, and you need to map the ``locale`` field. Then you can +use the ``translated`` attribute on all fields that should be different +depending on the locale. + +.. configuration-block:: + + .. code-block:: php + + // src/App/Documents/Article.php + namespace App\Documents\Article; + + use Doctrine\ODM\PHPCR\Mapping\Attributes as PHPCR; + + #[PHPCR\Document(translator: 'attribute')] + class Article + { + /** + * The language this document currently is in + */ + #[PHPCR\Locale] + private $locale; + + /** + * Untranslated property + */ + #[PHPCR\Date] + private $publishDate; + + /** + * Translated property + */ + #[PHPCR\Field(type: 'string', translated: true)] + private $topic; + + /** + * Language specific image + */ + #[PHPCR\Binary(translated: true)] + private $image; + } + + .. code-block:: xml + + + + + + + + + + + .. code-block:: yaml + + App\Documents\Article: + translator: attribute + locale: locale + fields: + publishDate: + type: date + topic: + type: string + translated: true + image: + type: binary + translated: true + +Unless you explicitly interact with the multi-language features of PHPCR-ODM, +documents are loaded in the request locale and saved in the locale they where +loaded. (This could be a different locale, if the PHPCR-ODM did not find the +requested locale and had to fall back to an alternative locale.) + +.. tip:: + + For more information on multilingual documents, see the + `PHPCR-ODM documentation on multi-language`_. + +.. _`PHPCR-ODM documentation on multi-language`: http://docs.doctrine-project.org/projects/doctrine-phpcr-odm/en/latest/reference/multilang.html +.. _`separate route per language`: https://symfony.com/bundles/CMFRoutingBundle/current/routing-component/dynamic.html diff --git a/doc/multiple_sessions.rst b/doc/multiple_sessions.rst new file mode 100644 index 0000000..f71cf8c --- /dev/null +++ b/doc/multiple_sessions.rst @@ -0,0 +1,348 @@ +.. index:: + single: Multisession; DoctrinePHPCRBundle + +Configuring multiple sessions for PHPCR-ODM +=========================================== + +If you need more than one PHPCR backend, you can define ``sessions`` as child +of the ``session`` information. Each session has a name and the configuration +following the same schema as what is directly in ``session``. You can also +overwrite which session to use as ``default_session``. Once you have multiple +sessions, you can also configure multiple document managers with those +sessions. + +.. tip:: + + Autowiring always gives you the default session and the default document + manager. When working with multiple sessions and managers, you need to + explicitly specify the services. For the document managers, you can also + go through the manager registry (see at the end of this page). + +.. _bundles-phpcr-odm-multiple-phpcr-sessions: + +Multiple PHPCR Sessions +----------------------- + +.. configuration-block:: + + .. code-block:: yaml + + # app/config/config.yml + doctrine_phpcr: + session: + default_session: ~ + sessions: + : + workspace: ... # Required + username: ~ + password: ~ + backend: + # ... + options: + # ... + + .. code-block:: xml + + + + + + + + + + + + + + + + + + .. code-block:: php + + // app/config/config.php + $container->loadFromExtension('doctrine_phpcr', [ + 'session' => [ + 'default_session' => null, + 'sessions' => [ + '' => [ + 'workspace' => '...', // Required + 'username' => null, + 'password' => null, + 'backend' => [ + // ... + ], + 'options' => [ + // ... + ], + ], + ], + ], + ]); + +Multiple Document Managers +-------------------------- + +If you are using the ODM, you will also want to configure multiple document +managers. + +Inside the odm section, you can add named entries in the +``document_managers``. To use the non-default session, specify the session +attribute. + +.. configuration-block:: + + .. code-block:: yaml + + # app/config/config.yml + odm: + default_document_manager: ~ + document_managers: + : + session: + # ... configuration as above + + .. code-block:: xml + + + + + + + + + + + + + + .. code-block:: php + + // app/config/config.php + $container->loadFromExtension('doctrine_phpcr', [ + 'odm' => [ + 'default_document_manager' => null, + 'document_managers' => [ + '' => [ + 'session' => '', + // ... configuration as above + ], + ], + ], + ]); + +Bringing it all together +------------------------ + +The following full example uses the default manager for ``AppBundle`` +and the documents provided by the CMF. Additionally, it has a website +and DMS manager that connects to the Jackrabbit of Magnolia CMS. That +manager looks for models in the MagnoliaBundle. + +.. configuration-block:: + + .. code-block:: yaml + + doctrine_phpcr: + # configure the PHPCR sessions + session: + sessions: + default: + backend: "%phpcr_backend%" + workspace: "%phpcr_workspace%" + username: "%phpcr_user%" + password: "%phpcr_pass%" + + website: + backend: + type: jackrabbit + url: "%magnolia_url%" + workspace: website + username: "%magnolia_user%" + password: "%magnolia_pass%" + + dms: + backend: + type: jackrabbit + url: "%magnolia_url%" + workspace: dms + username: "%magnolia_user%" + password: "%magnolia_pass%" + + # enable the ODM layer + odm: + auto_generate_proxy_classes: "%kernel.debug%" + document_managers: + default: + session: default + mappings: + AppBundle: ~ + CmfContentBundle: ~ + CmfMenuBundle: ~ + CmfRoutingBundle: ~ + + website: + session: website + configuration_id: magnolia.odm_configuration + mappings: + MagnoliaBundle: ~ + + dms: + session: dms + configuration_id: magnolia.odm_configuration + mappings: + MagnoliaBundle: ~ + + .. code-block:: xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + .. code-block:: php + + // app/config/config.php + $container->loadFromExtension('doctrine_phpcr', [ + 'session' => [ + 'sessions' => [ + 'default' => [ + 'backend' => '%phpcr_backend%', + 'workspace' => '%phpcr_workspace%', + 'username' => '%phpcr_user%', + 'password' => '%phpcr_pass%', + ], + 'website' => [ + 'backend' => [ + 'type' => 'jackrabbit', + 'url' => '%magnolia_url%', + ], + 'workspace' => 'website', + 'username' => '%magnolia_user%', + 'password' => '%magnolia_pass%', + ], + 'dms' => [ + 'backend' => [ + 'type' => 'jackrabbit', + 'url' => '%magnolia_url%', + ], + 'workspace' => 'dms', + 'username' => '%magnolia_user%', + 'password' => '%magnolia_pass%', + ], + ], + ], + + // enable the ODM layer + 'odm' => [ + 'auto_generate_proxy_classes' => '%kernel.debug%', + 'document_managers' => [ + 'default' => [ + 'session' => 'default', + 'mappings' => [ + 'AppBundle' => null, + 'CmfContentBundle' => null, + 'CmfMenuBundle' => null, + 'CmfRoutingBundle' => null, + ], + ], + 'website' => [ + 'session' => 'website', + 'configuration_id' => 'magnolia.odm_configuration', + 'mappings' => [ + 'MagnoliaBundle' => null, + ], + ], + 'dms' => [ + 'session' => 'dms', + 'configuration_id' => 'magnolia.odm_configuration', + 'mappings' => [ + 'MagnoliaBundle' => null, + ], + ], + ], + ], + ]); + + +You can access the managers through the manager registry available in the +service ``Doctrine\Bundle\PHPCRBundle\ManagerRegistry``:: + + use Doctrine\Bundle\PHPCRBundle\ManagerRegistry; + + /** @var $container \Symfony\Component\DependencyInjection\ContainerInterface */ + + // get the named manager from the registry + $dm = $container->get(ManagerRegistry::class)->getManager('website'); + + // get the manager for a specific document class + $dm = $container->get(ManagerRegistry::class)->getManagerForClass('CmfContentBundle:StaticContent'); + +Additionally, each manager is available as a service in the DI container. +The service name pattern is ``doctrine_phpcr.odm._document_manager`` so for +example the website manager is called +``doctrine_phpcr.odm.website_document_manager``.