diff --git a/crud/configuration/index.html b/crud/configuration/index.html index 269e066..eaa6627 100644 --- a/crud/configuration/index.html +++ b/crud/configuration/index.html @@ -1188,6 +1188,15 @@ + + +
setSortableCollectionProperty(string $sortableCollectionProperty)
In order to sort entities, the default property used is sortOrder. You can set something else.
setListRowAttributes(string $context, array $attributes)
Add attributes on a row (list).
+$configuration->setListRowAttributes('index', [
+ 'class' => 'foo',
+ 'data-foo' => function(Entity $entity) {
+ return $entity->getFoo(); // A string
+ },
+]);
+setBatchAction(string $context, string $action, string $label, callable $callack)
Add a batch action. The callback has 2 arguments:
@@ -2596,17 +2624,17 @@ Compatible withuse App\Core\Entity\EntityInterface;
-use App\Core\Manager\EntityManager;
-
-$configuration->setBatchAction(
- 'index',
- 'delete',
- 'Delete',
- function(EntityInterface $entity, EntityManager $manager) {
- $manager->delete($entity);
- }
-);
+use App\Core\Entity\EntityInterface;
+use App\Core\Manager\EntityManager;
+
+$configuration->setBatchAction(
+ 'index',
+ 'delete',
+ 'Delete',
+ function(EntityInterface $entity, EntityManager $manager) {
+ $manager->delete($entity);
+ }
+);
setGlobalBatchAction
setGlobalBatchAction(string $context, string $action, string $label, callable $callack)
@@ -2618,20 +2646,20 @@ Compatible with use App\Core\Entity\RepositoryQuery;
-use App\Core\Manager\EntityManager;
-use Symfony\Component\HttpFoundation\JsonResponse;
-
-$configuration->setGlobalBatchAction(
- 'index',
- 'export_json',
- 'Export to JSON',
- function(RepositoryQuery $query, EntityManager $manager, ?array $selection): JsonResponse {
- $items = $selection ?? $query->find();
-
- return $this->json($items);
- }
-);
+use App\Core\Entity\RepositoryQuery;
+use App\Core\Manager\EntityManager;
+use Symfony\Component\HttpFoundation\JsonResponse;
+
+$configuration->setGlobalBatchAction(
+ 'index',
+ 'export_json',
+ 'Export to JSON',
+ function(RepositoryQuery $query, EntityManager $manager, ?array $selection): JsonResponse {
+ $items = $selection ?? $query->find();
+
+ return $this->json($items);
+ }
+);
diff --git a/search/search_index.json b/search/search_index.json
index de12b6f..57f11fe 100644
--- a/search/search_index.json
+++ b/search/search_index.json
@@ -1 +1 @@
-{"config":{"lang":["en"],"separator":"[\\s\\-]+","pipeline":["stopWordFilter"]},"docs":[{"location":"","title":"Welcome to Murph","text":"Murph is an open-source CMF built on top of Symfony that helps you to build your own CMS with several domains and languages. It comes with a fully implemented and customizable tree manager, a CRUD generator, a 2FA authentication, settings and tasks managers, basic web analytics.
Symfony developers will love build on Murph \ud83d\udcaa End users will be fond of the interface and the powerful tools \ud83d\udc9c
Developed with love by Simon Vieille.
Support: Murph project on Matrix.
Access the demo.
"},{"location":"abtesting/","title":"A/B Testing","text":""},{"location":"abtesting/#overview","title":"Overview","text":"Murph contains a basic tools to create A/B Tests.
The logic of the implement follows this logic:
- A node can be configured to enable A/B Test. You also define a code to set the name of the test
- When Murph receives a request, an event is dispatched and contains an
App\\Core\\Ab\\AbTestInterface object - You must create an event subscriber and set variations defined by
- a name (a string)
- a value (whatever you want)
- a percentage chance (optional)
- Then a variation is picked and saved in user cookies
- Finally, you can retrieve the picked variation PHP side and template side
"},{"location":"abtesting/#configure-the-node","title":"Configure the node","text":"Go the navigation and edit the tested node:
- Enable A/B testing
- Define a code (eg:
example_test)
"},{"location":"abtesting/#the-event-subscriber","title":"The Event Subscriber","text":"The event subscriber helps you to define each variation and the TTL.
src/EventSubscriber/MyAbTestEventSubscriber.phpnamespace App\\EventSubscriber;\n\nuse App\\Core\\EventSubscriber\\AbEventSubscriber as EventSubscriber;\nuse App\\Core\\Event\\Ab\\AbTestEvent;\n\nclass MyAbTestEventSubscriber extends EventSubscriber\n{\n public function onInit(AbTestEvent $event)\n {\n if ($event->getTest()->getName() !== 'example_test') {\n return;\n }\n\n $event->getTest()\n ->addVariation('test_1', 'Value #1', 20) // 20% of chance\n ->addVariation('test_2', 'Value #2', 30) // 30% of chance\n ->addVariation('test_3', 'Value #3', 50) // 50% of chance\n ->setDuration(3600 * 24) // duration of the cookie in seconds\n ;\n }\n\n public function onRun(AbTestEvent $event)\n {\n // executed if a variation is newly picked\n }\n}\n
"},{"location":"abtesting/#the-result","title":"The result","text":"you can retrieve the test and the variation picked in PHP side and in template side.
use App\\Core\\Ab\\AbContainerInterface;\n\npublic function foo(AbContainerInterface $testContainer)\n{\n if ($testContainer->has('example_test')) {\n $test = $testContainer->get('example_test');\n\n $result = $test->getResult(); // eg: \"test_2\"\n $value = $test->getResultValue(); // eg: \"Value #2\"\n\n // ...\n }\n\n // ...\n}\n
{% if ab_test_exists('example_test') %}\n {% set test = ab_test('example_test') %}\n {% set result = ab_test_result('example_test') %}\n {% set value = ab_test_value('example_test') %}\n\n {# ... #}\n{% endif %}\n
"},{"location":"abtesting/#global-ab-test","title":"Global A/B Test","text":"If you need to perform an A/B test everywhere, you need to create a specific listener:
src/EventListener/CustomAbListener.phpnamespace App\\EventListener;\n\nuse App\\Core\\EventListener\\AbListener as EventListener;\nuse Symfony\\Component\\HttpFoundation\\Request;\nuse Symfony\\Component\\HttpKernel\\Event\\RequestEvent;\n\nclass CustomAbListener extends EventListener\n{\n /**\n * {@inheritdoc}\n */\n protected function supports(Request $request): bool\n {\n return true;\n }\n\n /**\n * {@inheritdoc}\n */\n protected function getAbTestCode(): string\n {\n return 'my_global_ab_test_code';\n }\n}\n
CustomAbListener must be registred:
config/services.ymlservices:\n # ...\n\n App\\EventListener\\CustomAbListener;\n tags:\n - { name: kernel.event_listener, event: kernel.request }\n - { name: kernel.event_listener, event: kernel.response }\n
"},{"location":"controller/","title":"Controller","text":""},{"location":"controller/#controller_1","title":"Controller","text":"The default controller of a node is App\\Core\\Controller\\Site\\PageController::show. PageController extends Symfony\\Bundle\\FrameworkBundle\\Controller\\AbstractController and implements very basic features: a Response builder which retrieves the good template and injects variables to the view.
To create a custom controller, do this way:
src/Controller/MyController.phpnamespace App\\Controller;\n\nuse App\\Core\\Controller\\Site\\PageController;\n\nclass MyController extends PageController\n{\n public function myAction()\n {\n if (!$this->siteRequest->getPage()) {\n throw $this->createNotFoundException();\n }\n\n return $this->defaultRender($this->siteRequest->getPage()->getTemplate(), [\n // view datas\n ]);\n }\n}\n
Then edit config/packages/app.yaml and add your controller:
core:\n site:\n controllers:\n - {name: 'My action', action: 'App\\Controller\\MyController::myAction'}\n
"},{"location":"controller/#urlgenerator","title":"UrlGenerator","text":"If your controller represents entities and if the associated node is visible in the sitemap, you can use a App\\Core\\Annotation\\UrlGenerator in annotations and implement a generator. See the example below.
src/UrlGenerator/MyEntityGeneratornamespace App\\UrlGenerator;\n\nuse App\\Core\\Entity\\Site\\Node;\nuse App\\Repository\\MyEntityRepositoryQuery;\nuse Symfony\\Component\\Routing\\Generator\\UrlGeneratorInterface;\n\nclass MyEntityGenerator\n{\n protected MyEntityRepositoryQuery $query;\n protected UrlGeneratorInterface $urlGenerator;\n\n public function __construct(MyEntityRepositoryQuery $query, UrlGeneratorInterface $urlGenerator)\n {\n $this->query = $query;\n $this->urlGenerator = $urlGenerator;\n }\n\n public function myActionGenerator(Node $node, array $options): array\n {\n $entities = $this->query->create()->find();\n\n $urls = [];\n\n foreach ($entities as $entity) {\n $urls[] = $this->urlGenerator->generate(\n $node->getRouteName(),\n [\n 'entity' => $entity->getId(),\n '_domain' => $options['_domain'],\n ],\n UrlGeneratorInterface::ABSOLUTE_URL\n );\n }\n\n return $urls;\n }\n}\n
Then, the have to annotate the controller like this (note: options is optional):
src/Controller/MyController.phpnamespace App\\Controller;\n\nuse App\\Core\\Annotation\\UrlGenerator;\nuse App\\Core\\Controller\\Site\\PageController;\nuse App\\UrlGenerator\\MyEntityGenerator;\n\nclass MyController extends PageController\n{\n #[UrlGenerator(service: MyEntityGenerator::class, method: 'myActionGenerator', options=[])]\n public function myAction(MyEntity $entity)\n {\n // do stuff\n }\n}\n
Finally, update config/services.yaml:
services:\n # ...\n\n App\\UrlGenerator\\MyEntityGenerator:\n public: true\n
"},{"location":"procedure/","title":"Installation","text":""},{"location":"procedure/#setting-up-the-skeleton","title":"Setting up the skeleton","text":"Depending of you environment, PHP and composer could be located in specific paths. Defines theme with environment vars:
export PHP_BIN=/usr/bin/php\nexport COMPOSER_BIN=/usr/bin/composer\nexport NPM_BIN=/usr/bin/npm\nexport YARN_BIN=/usr/bin/yarn\n
Create your project:
\"$COMPOSER_BIN\" create-project murph/murph-skeleton my_project ^1\n
An error occured because of the unconfigured database.
- Copy
.env into .env.local - Edit
.env.local (documentation). Don't forget to set APP_SECRET. - Run
make build
"},{"location":"procedure/#create-an-admin-user","title":"Create an admin user","text":"Run \"$PHP_BIN\" bin/console murph:user:create and answer questions.
"},{"location":"procedure/#configure-a-web-server","title":"Configure a web server","text":"Read the documentation of Symfony to configure a web server.
In case of a local server, you can use the Symfony Local Web Server. Then go to https://127.0.0.1:8000/admin.
"},{"location":"requirements/","title":"Technical Requirements","text":" - Install PHP 8.0 or higher and these PHP extensions (which are installed and enabled by default in most PHP 8 installations): Ctype, iconv, JSON, PCRE, Session, SimpleXML, and Tokenizer;
- Install Composer, which is used to install PHP packages.
- Install NodeJS 16 or higher, Yarn and Webpack
- A created database (MySQL, PostgreSQL or SQLite)
- build-essential to run
make
"},{"location":"sources/","title":"Sources codes","text":"Source codes are accessible on Gitnet:
- Core
- Source code the skeleton
"},{"location":"tasks/","title":"Tasks","text":"Tasks are scripts executabled from UI. The creation of tasks is based on events.
src/EventSubscriber/MyTaskEventSubscriber.phpnamespace App\\EventSubscriber;\n\nuse App\\Core\\Event\\Task\\TaskInitEvent;\nuse App\\Core\\Event\\Task\\TaskRunRequestedEvent;\nuse App\\Core\\EventSubscriber\\Task\\TaskEventSubscriber;\n\nclass MyTaskEventSubscriber extends TaskEventSubscriber\n{\n public function onInit(TaskInitEvent $event)\n {\n $event->addTask('my_task', 'Example', 'My task');\n }\n\n public function onRunRequest(TaskRunRequestedEvent $event)\n {\n if ('my_task' !== $event->getTask()) {\n return;\n }\n\n $event->getOutput()->writeln('My task is started');\n\n // ...\n\n $event->getOutput()->writeln('My task is finished');\n }\n}\n
"},{"location":"template/","title":"Templating","text":""},{"location":"template/#variables","title":"Variables","text":"By default, these variables are given to a CMS view:
- The current node is
_node and its menu is _menu - The current navigation is
_navigation - The current locale is
_locale - The CMS store is
_store
"},{"location":"template/#navigations-and-menus","title":"Navigations and menus","text":" - Retrieve all navigations:
_store.navigations -
Retrieve a navigation by its code: _store.navigation('the_code')
-
Retrieve all navigation menus: _navigation.menus
- Retrieve a menu by its code:
_navigation.menu('the_code') - Retrieve all nodes of a menu:
menu.rootNode.children -
Retrieve visible nodes of a menu: menu.rootNode.children({visible: true})
-
Test if a node is the current one: _store.isActiveNode(node)
- Test if a node is or contains the current one:
_store.isActiveNode(node, true)
"},{"location":"template/#page","title":"Page","text":"You can access a page's blocks this way:
{% set myBlock = _page.myBlock.value %}\n\n{{ myBlock }}\n
"},{"location":"template/#url-and-path","title":"URL and path","text":"Murph has somes twig functions to manage URLs:
"},{"location":"template/#generic-functions","title":"Generic functions","text":" - Same as twig url but catches all exceptions:
{{ safe_url(routeName, options, relative) }} - Same as twig path but catches all exceptions:
{{ safe_path(routeName, options, relative) }}
"},{"location":"template/#node-functions","title":"Node functions","text":" - Generates a URL using a node:
{{ node_url(node, options, relative) }} - Generates a path using a node:
{{ node_path(node, options, relative) }} - Generates a URL using a node and catches all exceptions:
{{ safe_node_url(node, options, relative) }} - Generates a path using a node and catches all exceptions:
{{ safe_node_path(node, options, relative) }}
A node may have a disabled URL:
{% if not node.disableUrl %}\n {% set path = safe_node_path(node) %}\n {% set url = safe_node_url(node) %}\n{% endif %}\n
When the navigation has several domains, you can specify the domain:
{% set path = safe_node_path(node, {_domain: _domain}) %}\n{% set url = safe_node_url(node, {_domain: _domain}) %}\n
"},{"location":"template/#code-functions","title":"Code functions","text":" - Generates a URL using codes:
{{ code_url(menuCode, nodeCode, options, relative) }} - Generates a path using codes:
{{ code_path(menuCode, nodeCode, options, relative) }} - Generates a URL using codes and catches all exceptions:
{{ safe_code_url(menuCode, nodeCode, options, relative) }} - Generates a path using codes and catches all exceptions:
{{ safe_code_path(menuCode, nodeCode, options, relative) }}
"},{"location":"template/#filters","title":"Filters","text":"When a content could contains tags (eg: '{{url://my_route}}), usemurph_url`. See the example below:
Code Output {{ content }} A link to the <a href=\"{{url://contact}}\">contact page</a> {{ content|murph_url }} A link to the <a href=\"https://example.com/contact\">contact page</a>"},{"location":"template/#string-builder","title":"String builder","text":"The string builder builds a string using a format and an object or an array.
Examples:
{{ 'Entity ID is {id}'|build_string(myEntity) }} will output: Entity ID is 42 {{ 'Hello, {user.displayName}!'|build_string(myEntity) }} will output Hello, John doe!
In case of an not accessible property, no exception will be thrown.
"},{"location":"template/#file-attributes","title":"File attributes","text":"Attributes are managed from the file manager. They are accessibles with these filters:
Code Result {{ '<img ... alt=\"{{fattr://hash/alt}}\">'|file_attributes }} <img ... alt=\"Attribute 'alt' of the file with the given hash\"> {{ 'path/to/file'|file_attribute('alt') }} Attribute alt of the given file"},{"location":"users/","title":"Users","text":"Murph provided a basic authentication based on the User entity.
"},{"location":"changelog/core/","title":"Changelog","text":"[v1.26.0] - 2025-03-17 Added
- FileUploadHandler: allow to upload multiple files
- CrudController: allow to add callables after creation, update and delation
Fixed
- fix(crud): use context variable to retrieve the form and the form options
- fix(node): use
false instead of O in query
Changed
- CrudConfiguration::setShowActions: add context when setting this parameter
[v1.25.2] - 2025-02-07 Added
- allow to set
data-* attributes on modal
[v1.25.1] - 2024-05-13 Added
- add drag&drop in the block builder
[v1.25.0] - 2024-05-12 Added
- add block builder widget
- allow to use
window.tinymceModes to add or override tinymce modes - add border color on tinymce editor
Fixed
- fix default crud sort
- fix hidden save button in file manager
- fix template of CrudController (maker)
- fix undefined
window.tinymce.murph
[v1.24.1] - 2024-02-01 Fixed
- update Murph version constant
[v1.24.0] - 2024-01-27 Added
- add CSS class
no-wrap - copy the pager of the CRUD at the bottom of the list
Fixed
- fix an issue with the file manager when editing an item opened in a modal
- fix type casting in slugifier
[v1.23.0] - 2023-11-01 Added
- allow to define templates show before and after a murph collection item
- add global batch actions
- add constraint
Length in forms - add sass classes to mange with of elements
- set searchFields option on jschoice manager (search on labels)
Changed
- refactor services using constructor property promotions
- remove twig in the mail notifier service
- change pills colors
- change border colors of inputs when focused
- change colors on js-choices element
Fixed
- fix regression on crud sorting
- fix test in RepositoryQuery::addForcedFilterHandler
- remove parameter $option on CrudConfiguration::setForm and fix CrudController make template
- fix the aspect of the actions's column in the crud
[v1.22.0] - 2023-09-28 Added
- add new options in BooleanField:
toggle|checkbox_class_when_true and toggle|checkbox_class_when_false - add
count method in repository query - add
addForcedFilterHandler method in repository query - add
inline_form_validation option to validate inline forms with custom algo - add crud sorting parameters in the session
- add flush option in the entity manager on create, update, remove, and persist methods
[1.21.1] - 2023-08-17 Added
- add form error handle in inline edit action and refill the form using the previous request content
- add form error handle in ssettings actions and refill the form using the previous request content
Fixed
- fix tinymce reload when modal is closed and reopened
- fix modal hiding when a file is successfuly uploaded in the file manager
[1.21.0] - 2023-08-11 Added
- allow to use array syntax in string builder filter
- add color property in Navigation
- add badge with navigation color in admin views
- add
default_value option in crud fields - add
display option in BooleanField - add associated nodes in page form
Fixed
- fix routes in the global settings controller
[1.20.0] - 2023-07-27 Added
- enable double click on cruds
- add block class name for the choice type in the page maker
- update file details view on the file manager
- add form options in the crud filter action
- add trans filter in inline form modal title
- add setter to define all fields in a defined context
- add filename generator setter in FileUploadHandler
- add variable for the sidebar size
- add twig block to override defaults actions in crud index template
- add option to remove iterable values and/or specifics keys in the twig toArray function
- add boolean field for CRUD
- add context variable in each controllers to simplify overrides
- core.site.name and core.site.logo are not longer required
- add default templates when a crud is generated
- add boolean 'is_disabled' in the menu item template options
Fixed
- fix filemanager date ordering
- fix maker CrudController template: remove bad pasted code
- fix redirect listener: use boolean instead of integer
- fix responsive of account edit template
- fix collection widget: allow_add/allow_delete and prototype
Changed
- user admin routes are defined in core, custom controller is not required
[1.19.0] - 2023-04-15 Added
- feat(page): forms for metas, opengraph and extra informations can be removed
- feat(navigation): user interface is improved
- feat(file): webp is allowed and shown in form widgets and in file manager details
- feat(file): the file manager now show the size and the modification date of a file
- feat(crud): add option
action in field to add a link to the view page or to the edition page - feat(crud): add option
inline_form in field to configure to edit the data - feat(crud): add
setDoubleClick in the crud configuration
[1.18.0] - 2023-01-13 Added
- feat(dep): add symfony/runtime
- feat(dep): add symfony/flex
Fixed
- fix(crud): allow POST in delete actions
- fix(crud): remove default page value in abstract crud controller
- fix(admin): test site_logo before using it
- fix(ui): update z-index of choices__list--dropdown
[1.17.1] - 2022-12-03 Fixed
- add mising attribute on timestampable (doctrine)
[1.17.0] - 2022-11-19 Fixed
- fix tinymce modal z-index in tox
Changed
- replace annotation with attributes
[1.16.0] - 2022-09-06 Added
- add A/B testing feature
- add cleanup of html string extracted from grapesjs content
Fixed
- fix file block type
Changed
- remove dashboard action from the core
[1.15.0] - 2022-05-09 Added
- CrudConfiguration::setAction can receive a callable instead of a boolean in 'enabled' param
- add grapesjs-component-code-editor and grapesjs-parser-postcss
- hide the backoffice site name when small resolution
- add entity_to_array twig function
- add default field to show in crud configuration
Fixed
- fix the mail notifier
- fix sitemap: navigation with several domains
- fix regression with editorjs: content not loaded
Changed
- change default template to show an entity using
entity_to_array
[1.14.1] - 2022-04-30 Added
- add allowed chars in RouteParameterSlugify and CodeSlugify
- improve sidebar in mobile view
Fixed
- fix creation of new element when a menu is edited
- fix editorjs error when the textarea is empty
[1.14.0] - 2022-04-20 Added
- add grapesjs modes
- add tinymce block type
- add editor types in page maker
- add the page template when the page is generated with the maker
Changed
- replace flag-icon-css with flag-icons
[1.13.0] - 2022-04-17 Added
- add editorjs hyperlink block
- add button to show and hide metas (admin)
- add grapesjs editor
- add editorjs type
Fixed
- fix editorjs inline tools (bold and italic)
Changed
- update editorjs quote block template
[1.12.0] - 2022-03-26 Added
- add page maker command (
make:page) - add CrudConfiguration::getViewData in complement of CrudConfiguration::getViewDatas
- add editorjs link block endpoint
Fixed
- fix issue with empty user-agent in AnalyticListener
Changed
- update editorjs image block view
[1.11.0] - 2022-03-22 Added
- add data-modal-create attribute to force modal to be open in a new container
- add blur when several modals are opened
- add specific form types for Tinymce and EditorJS
Changed
- update file-manager with data-modal-create attribute
[1.10.0] - 2022-03-17 Added
- add url and path generators using code (twig)
Changed
- update node entity constraints
[1.9.2] - 2022-03-14 Fixed
- fix issue with murph version constant and autoloader
[1.9.1] - 2022-03-14 Added
- add murph version in autoload file
Changed
- remove AdminController constructor
[1.9.0] - 2022-03-13 Added
- add murph version in admin ui
Changed
- the core is now installed with composer
[1.8.0] - 2022-03-10 Added
- add security roles in app configuration
- add option to restrict node access to specific roles
Changed
- rename
core/EventSuscriber with core/EventSubscriber
[1.7.3] - 2022-03-06 Added
- add ability to rename file in the file manager
Fixed
- fix user factory
- fix user creation from ui
[1.7.2] - 2022-03-03 Added
- add templates to render sections and items in the admin menu
Fixed
- fix the analytic table when a path is a long
[1.7.1] - 2022-03-01 Added
- add translations
Fixed
- fix missing directories
[1.7.0] - 2022-03-01 Fixed
- fix the analytic referers table when a referer has a long domain
Changed
- upgrade dependencies
- move assets to the core directory
[1.6.0] - 2022-02-28 Added
- add block in field templates to allow override
- merge route params in crud admin redirects
- improve murph:user:create command
Fixed
- fix form namespace prefix in the crud controller maker
- fix date field when the value is empty
- fix crud batch column width
- fix sidebar icon width
- fix cache clear task
Changed
- remove password generation from the user factory
[1.5.0] - 2022-02-25 Added
- add desktop views and mobile views
Changed
- upgrade dependencies
- replace jaybizzle/crawler-detect with matomo/device-detector
[1.4.1] - 2022-02-23 Added
- handle app urls in twig routing filters
Fixed
- fix views in analytics modal
- replace empty path with \"/\" in analytics
Changed
- update default templates
[1.4.0] - 2022-02-21 Added
- add basic analytics
[1.3.0] - 2022-02-19 Added
- add support of regexp with substitution in redirect
- url tags can be used as redirect location
- add builders to replace file information tags and url tags
Fixed
- fix filemanager sorting
- fix batch action setter
[1.2.0] - 2022-02-14 Added
- add sort in file manager
- add redirect manager
Changed
- replace node-sass with sass
[1.1.0] - 2022-02-29 Added
- add directory upload in file manager
Fixed
- fix admin node routing
Changed
- symfony/swiftmailer-bundle is replaced by symfony/mailer
[1.0.1] - 2022-02-25 Fixed
- fix Makefile environment vars (renaming)
- fix composer minimum stability
[1.0.0] - 2022-01-23"},{"location":"changelog/skeleton/","title":"Changelog","text":"[v1.23.0] - 2023-09-28 Changed
- upgrade murph/murph-core
[v1.22.0] - 2023-09-28 Added
- update woodpecker ci base file
Fixed
- fix #1: add UniqueEntity constraint in the User entity
Changed
- upgrade murph/murph-core
[1.21.0] - 2023-08-11 Changed
- upgrade murph/murph-core
[1.20.0] - 2023-07-27 Fixed
- fix collection widget: allow_add/allow_delete and prototype
Added
- add user admin controller and simples views in default files
- add chdir in the console entrypoint
Changed
- upgrade murph/murph-core
[1.19.0] - 2023-04-15 Changed
- upgrade murph/murph-core
[1.18.0] - 2023-01-13 Added
- feat(dep): update dependencies
- feat(update): apply new recipe for phpunit
- feat(update): apply recipes:update doctrine/doctrine-bundle
- feat(update): apply recipes:update doctrine/doctrine-migrations-bundle
- feat(update): apply recipes:update liip/imagine-bundle
- feat(update): apply recipes:update stof/doctrine-extensions-bundle
- feat(update): apply recipes:update symfony/apache-pack
- feat(update): apply recipes:update symfony/console
- feat(update): apply recipes:update symfony/debug-bundle
- feat(update): apply recipes:update symfony/flex
- feat(update): apply recipes:update symfony/mailer
- feat(update): apply recipes:update symfony/framework-bundle
- feat(update): apply recipes:update symfony/monolog-bundle
- feat(update): apply recipes:update symfony/routing
- feat(update): apply recipes:update symfony/security-bundle
- feat(update): apply recipes:update symfony/translation
- feat(update): apply recipes:update symfony/twig-bundle
- feat(update): apply recipes:update symfony/validator
- feat(update): apply recipes:update symfony/web-profiler-bundle
- feat(update): apply recipes:update symfony/webpack-encore-bundle
- feat(update): apply recipes:update scheb/2fa-bundle
Fixed
- fix(config): fix typo in 2fa conf
- fix(config): fix firewall config
[1.17.0] - 2022-11-19 Changed
- upgrade murph/murph-core
- replace annotation with attributes
- use encore from node_modules in npm scripts
[1.16.0] Added
- add a admin dashboard controller
- add meta description in base.html.twig
Fixed
Changed
- upgrade murph/murph-core
[1.15.0] Changed
- upgrade murph/murph-core
[1.14.3] Added
- add blocks in default template
Changed
- upgrade murph/murph-core
[1.14.2] [1.14.1] Fixed
- fix missing envvar in makefile (npm)
[1.14.0] Changed
- upgrade murph/murph-core
[1.13.0] Changed
- upgrade murph/murph-core
[1.12.0] Changed
- upgrade murph/murph-core
[1.11.0] Changed
- upgrade murph/murph-core
- use murph-npm to install npm requirements
[1.10.0] Added
- add translated title in dashboard template
Fixed
- remove useless env var from makefile
Changed
- upgrade murph/murph-core
[1.9.1] - 2022-03-14 Added
- add murph version in autoload file
Changed
- remove AdminController constructor
[1.9.0] - 2022-03-13 Added
- add murph version in admin ui
Changed
- the core is now installed with composer
[1.8.0] - 2022-03-10 Added
- add security roles in app configuration
- add option to restrict node access to specific roles
Changed
- rename
core/EventSuscriber with core/EventSubscriber
[1.7.3] - 2022-03-06 Added
- add ability to rename file in the file manager
Fixed
- fix user factory
- fix user creation from ui
[1.7.2] - 2022-03-03 Added
- add templates to render sections and items in the admin menu
Fixed
- fix the analytic table when a path is a long
[1.7.1] - 2022-03-01 Added
- add translations
Fixed
- fix missing directories
[1.7.0] - 2022-03-01 Fixed
- fix the analytic referers table when a referer has a long domain
Changed
- upgrade dependencies
- move assets to the core directory
[1.6.0] - 2022-02-28 Added
- add block in field templates to allow override
- merge route params in crud admin redirects
- improve murph:user:create command
Fixed
- fix form namespace prefix in the crud controller maker
- fix date field when the value is empty
- fix crud batch column width
- fix sidebar icon width
- fix cache clear task
Changed
- remove password generation from the user factory
[1.5.0] - 2022-02-25 Added
- add desktop views and mobile views
Changed
- upgrade dependencies
- replace jaybizzle/crawler-detect with matomo/device-detector
[1.4.1] - 2022-02-23 Added
- handle app urls in twig routing filters
Fixed
- fix views in analytics modal
- replace empty path with \"/\" in analytics
Changed
- update default templates
[1.4.0] - 2022-02-21 Added
- add basic analytics
[1.3.0] - 2022-02-19 Added
- add support of regexp with substitution in redirect
- url tags can be used as redirect location
- add builders to replace file information tags and url tags
Fixed
- fix filemanager sorting
- fix batch action setter
[1.2.0] - 2022-02-14 Added
- add sort in file manager
- add redirect manager
Changed
- replace node-sass with sass
[1.1.0] - 2022-02-29 Added
- add directory upload in file manager
Fixed
- fix admin node routing
Changed
- symfony/swiftmailer-bundle is replaced by symfony/mailer
[1.0.1] - 2022-02-25 Fixed
- fix Makefile environment vars (renaming)
- fix composer minimum stability
[1.0.0] - 2022-01-23"},{"location":"crud/","title":"CRUD","text":"Murph helps you to manage specific entities with a CRUD manager:
- Create entities
- Read (Show) entities
- Update entities
- Delete entities
You can configure almost anything:
- How to list entities:
- Information to show
- Available actions
- Filters
- Sorting
- ...
- How to show an entity
- How to create and update an entity
"},{"location":"crud/configuration/","title":"Configuration","text":"A generated crud controller contains a method named getConfiguration. This methods returns a instance of App\\Core\\Crud\\CrudConfiguration.
"},{"location":"crud/configuration/#setpagetitle","title":"setPageTitle","text":"setPageTitle(string $page, string $title)
Set the title of the given page.
Example of usage in a CRUD template: <title>{{ configuration.pageTitle('index') }}</title>.
"},{"location":"crud/configuration/#setpageroute","title":"setPageRoute","text":"setPageRoute(string $page, string $route)
Set the route of the given page. By default, pages are: index, edit, new, show. You can create a custom page for a custom controller.
Example of usage in a CRUD template: <a href=\"{{ path(configuration.pageRoute('new')) }}\">...</a>.
"},{"location":"crud/configuration/#setform","title":"setForm","text":"setForm(string $context, string $form,array$options = [])
Set the form used in the given context.
"},{"location":"crud/configuration/#setformoptions","title":"setFormOptions","text":"setFormOptions(string $context,array$options = [])
Defines options given to a form.
"},{"location":"crud/configuration/#setaction","title":"setAction","text":"setAction(string $page, string $action, bool|callable $enabled)
Set if an action is enabled or not in the given page. Take a look at core/Resources/views/admin/crud/*.html.twig for more information. Depending the context, the callable could receive the entity in parameter. Example:
->setAction('index', 'edit', function(EntityInterface $entity) {\n return $entity->getUser()->getId() === $this->getUser()->getId();\n})\n
Usage in a CRUD template: {% if configuration.action('index', 'new')%}...{% endif %}.
"},{"location":"crud/configuration/#setactiontitle","title":"setActionTitle","text":"setActionTitle(string $page, string $action, string $title)
Set the title of an action in the given page.
Example of usage in a CRUD template: {{ configuration.actionTitle(context, 'new', 'New')|trans }}
"},{"location":"crud/configuration/#setview","title":"setView","text":"setView(string $context, string $view)
Override a view.
Controller (context) View Description index @Core/admin/crud/index.html.twig Template of the page index edit @Core/admin/crud/edit.html.twig Template of the page edit new @Core/admin/crud/new.html.twig Template of the page new show @Core/admin/crud/show.html.twig Template of the page show filter @Core/admin/crud/filter.html.twig Template of the page filter Form (context) View Description form @Core/admin/crud/_form.html.twig Template to render a form form_widget @Core/admin/crud/_form_widget.html.twig Template to render a form widget form_translations @Core/admin/crud/_form_translations.html.twig Template to render a the translation field Entity (context) View Description show_entity @Core/admin/crud/_show.html.twig Template to render the entity"},{"location":"crud/configuration/#setshowactions","title":"setShowActions","text":"setShowActions(string $context, bool $value)
Show or hide the column Actions.
"},{"location":"crud/configuration/#setviewdatas","title":"setViewDatas","text":"setViewDatas(string $context,array$datas) and addViewData(string $context, string $name, $value)
Add datas given to a view. Useful in a custom controller.
"},{"location":"crud/configuration/#setfield","title":"setField","text":"setField(string $context, string $label, string $field,array$options)
Add a field displayed in the given context. Used in the index.
use App\\Core\\Crud\\Field;\n\n$configuration->setField('index', 'Title', Field\\TextField::class, [\n // options\n])\n
All fields have these options:
Option Type Default Description property string null Entity's property to display property__builder callable null A callable data and used to generate the content displayed view string @Core/admin/crud/field/text.html.twig The templated rendered default_value string null Default value to display when the property is null action string null An action to perform on click (null, edit, view) raw boolean false Render as HTML sort array | callable null Defines how to sort href string | callable null Data to generate a link href_attr array | callable null Attributes of the link inline_form null | callable null A method to define a form to edit datas inline_form_validation null | callable null A method to define a custom form validation callback Example #0$configuration->setField('index', 'My field', TextField::class, [\n 'property' => 'myProperty',\n // OR\n 'property_builder' => function($entity, array $options) {\n return $entity->getMyProperty();\n },\n])\n
Example #1$configuration->setField('index', 'My field', TextField::class, [\n 'raw' => true,\n 'property_builder' => function($entity, array $options) {\n return sprintf('<span class=\"foo\">%s</span>', $entity->getBar());\n },\n])\n
Example #2// https://127.0.0.7:8000/admin/my_entity?_sort=property&_sort_direction=asc\n$configuration->setField('index', 'My field', TextField::class, [\n 'property' => 'myProperty'\n 'sort' => ['property', '.myProperty'],\n // OR\n 'sort' => ['property', function(MyEntityRepositoryQuery $query, $direction) {\n $query->orderBy('.myProperty', $direction);\n }],\n])\n
Example #3use Symfony\\Component\\Form\\FormBuilderInterface;\nuse Symfony\\Component\\Form\\Extension\\Core\\Type\\TextType;\nuse Symfony\\Component\\Validator\\Constraints\\NotBlank;\n\n$configuration->setField('index', 'My field', TextField::class, [\n 'property' => 'myProperty',\n 'inline_form' => function(FormBuilderInterface $builder, EntityInterface $entity) {\n $builder->add('myProperty', TextType::class, [\n 'required' => true,\n 'constraints' => [new NotBlank()],\n ]);\n }\n])\n
"},{"location":"crud/configuration/#textfield","title":"TextField","text":"App\\Core\\Crud\\Field\\TextField
Option Type Default Description view string @Core/admin/crud/field/text.html.twig The templated rendered"},{"location":"crud/configuration/#datefield","title":"DateField","text":"App\\Core\\Crud\\Field\\DateField
Option Type Default Description view string @Core/admin/crud/field/date.html.twig The templated rendered format string Y-m-d The date format"},{"location":"crud/configuration/#datetimefield","title":"DatetimeField","text":"App\\Core\\Crud\\Field\\DatetimeField
Option Type Default Description view string @Core/admin/crud/field/date.html.twig The templated rendered format string Y-m-d H:i:s The date format"},{"location":"crud/configuration/#buttonfield","title":"ButtonField","text":"App\\Core\\Crud\\Field\\ButtonField
Option Type Default Description view string @Core/admin/crud/field/button.html.twig The templated rendered button_attr array [] Button HTML attributes button_attr_builder callabled null A callable data and used to generate button__attr button_tag string button HTML tag of the button"},{"location":"crud/configuration/#imagefield","title":"ImageField","text":"App\\Core\\Crud\\Field\\ImageField
Option Type Default Description view string @Core/admin/crud/field/image.html.twig The templated rendered image_attr array [] Image HTML attributes"},{"location":"crud/configuration/#booleanfield","title":"BooleanField","text":"App\\Core\\Crud\\Field\\BooleanField
Option Type Default Description view string @Core/admin/crud/field/boolean.html.twig The templated rendered display string toggle Type of render (toggle or checkbox) checkbox_class_when_true string fa-check-square HTML class added when the value is true and display is checkbox checkbox_class_when_false string fa-square HTML class added when the value is false and display is checkbox toggle_class_when_true string bg-success HTML class added when the value is true and display is toggle toggle_class_when_false string bg-secondary HTML class added when the value is false and display is toggle"},{"location":"crud/configuration/#setmaxperpage","title":"setMaxPerPage","text":"setMaxPerPage(string $page, int $max)
Set how many elements are displayed in a single page.
"},{"location":"crud/configuration/#seti18n","title":"setI18n","text":"setI18n(array $locales, string $defaultLocale)
Set an array of locales for a translatable entity. The default locale is used in the index page. Compatible with https://github.com/KnpLabs/DoctrineBehaviors/blob/master/docs/translatable.md.
"},{"location":"crud/configuration/#setdefaultsort","title":"setDefaultSort","text":"setDefaultSort(string $context, string $label, string $direction = 'asc')
Set the default sort applied in the repository query.
$configuration\n ->setDefaultSort('index', 'title', 'asc')\n ->setField('index', 'Title', Field\\TextField::class, [\n 'property' => 'title',\n 'sort' => ['title', '.title'],\n ]);\n
"},{"location":"crud/configuration/#setissortablecollection","title":"setIsSortableCollection","text":"setIsSortableCollection(string $page, bool $isSortableCollection)
It enables the drag & drop to sort entities.
class MyEntity implements EntityInterface\n{\n // ...\n\n /**\n * @ORM\\Column(type=\"integer\", nullable=true)\n */\n private $sortOrder;\n\n public function getSortOrder(): ?int\n {\n return $this->sortOrder;\n }\n\n public function setSortOrder(?int $sortOrder): self\n {\n $this->sortOrder = $sortOrder;\n\n return $this;\n }\n\n // ...\n}\n
"},{"location":"crud/configuration/#setsortablecollectionproperty","title":"setSortableCollectionProperty","text":"setSortableCollectionProperty(string $sortableCollectionProperty)
In order to sort entities, the default property used is sortOrder. You can set something else.
"},{"location":"crud/configuration/#setbatchaction","title":"setBatchAction","text":"setBatchAction(string $context, string $action, string $label, callable $callack)
Add a batch action. The callback has 2 arguments:
- An instance of
App\\Core\\Entity\\EntityInterface - An instance of
App\\Core\\Manager\\EntityManager
use App\\Core\\Entity\\EntityInterface;\nuse App\\Core\\Manager\\EntityManager;\n\n$configuration->setBatchAction(\n 'index',\n 'delete',\n 'Delete',\n function(EntityInterface $entity, EntityManager $manager) {\n $manager->delete($entity);\n }\n);\n
"},{"location":"crud/configuration/#setglobalbatchaction","title":"setGlobalBatchAction","text":"setGlobalBatchAction(string $context, string $action, string $label, callable $callack)
Add a global batch action. The callback has 3 arguments:
- An instance of
App\\Core\\Repository\\RepositoryQuery - An instance of
App\\Core\\Manager\\EntityManager - An array of selected entities or a
null value
Do not use the same action in global and classic batch action.
The callback can return a response. If not, the user will be redirect automatically. See the example below:
use App\\Core\\Entity\\RepositoryQuery;\nuse App\\Core\\Manager\\EntityManager;\nuse Symfony\\Component\\HttpFoundation\\JsonResponse;\n\n$configuration->setGlobalBatchAction(\n 'index',\n 'export_json',\n 'Export to JSON',\n function(RepositoryQuery $query, EntityManager $manager, ?array $selection): JsonResponse {\n $items = $selection ?? $query->find();\n\n return $this->json($items);\n }\n);\n
"},{"location":"crud/generator/","title":"Generator","text":""},{"location":"crud/generator/#prepare-your-entity","title":"Prepare your entity","text":" - implements
App\\Core\\Entity\\EntityInterface (see Entity Manager) - creates a repository query (see Repository Query)
- creates a factory (see Factory)
- generates a form (see
php bin/console make:form --help)
"},{"location":"crud/generator/#generation","title":"Generation","text":"The generation is performed in CLI. These information are required:
- The name of the futur controller (eg:
MyEntityAdminController) - The namespace of the entity (eg:
MyEntity) - The namespace of the entity repository query (eg:
MyEntityRepositoryQuery) - The namespace of the the entity factory (eg:
MyEntityFactory) - The namespace of the form used to create and update the entity (eg:
MyEntityType)
Simply run php bin/console make:crud-controller.
"},{"location":"entities/em/","title":"Entity Manager","text":"The entity manager of Muprh is a proxy of the Doctrine's entity manager. It gives you an easy way to create, update and delete an entity and dispatches events easy to subscribe to.
"},{"location":"entities/em/#implementation","title":"Implementation","text":"Entities must implements App\\Core\\Entity\\EntityInterface.
src/Entity/MyEntity.phpnamespace App\\Entity;\n\nuse App\\Repository\\MyEntityRepository;\nuse Doctrine\\ORM\\Mapping as ORM;\nuse App\\Core\\Entity\\EntityInterface;\n\n#[ORM\\Entity(repositoryClass: MyEntityRepository::class)]\nclass MyEntity implements EntityInterface\n{\n // ...\n}\n
"},{"location":"entities/em/#usage","title":"Usage","text":"There are 2 entity managers which are services:
App\\Core\\Manager\\EntityManager used for all entities App\\Core\\Manager\\TranslatableEntityManager used for translatable entities
src/Controller/FooController.phpnamespace App\\Controller;\n\nuse App\\Core\\Manager\\EntityManager\nuse App\\Entity\\MyEntity;\nuse Symfony\\Component\\HttpFoundation\\Response;\n\nclass FooController\n{\n public function foo(EntityManager $entityManager): Response\n {\n $myEntity = new MyEntity();\n\n // Creates an entity\n $entityManager->create($myEntity);\n\n // Updates an entity\n $entityManager->update($myEntity);\n\n // Deletes an entity\n $entityManager->delete($myEntity);\n\n // ...\n }\n}\n
"},{"location":"entities/em/#events","title":"Events","text":"Events are dispatched before and after creation, update and deletion. All entities of Muprh use the entity manager.
src/EventSubscriber/MyEntityEventSubscriber.phpnamespace App\\EventSubscriber;\n\nuse App\\Core\\Entity\\EntityInterface;\nuse App\\Core\\Event\\EntityManager\\EntityManagerEvent;\nuse App\\Core\\EventSubscriber\\EntityManagerEventSubscriber;\nuse App\\Entity\\MyEntity;\n\nclass MyEntityEventSubscriber extends EntityManagerEventSubscriber\n{\n public function supports(EntityInterface $entity): bool\n {\n return $entity instanceof MyEntity;\n }\n\n public function onCreate(EntityManagerEvent $event)\n {\n if (!$this->supports($event->getEntity())) {\n return;\n }\n\n // ...\n }\n\n public function onUpdate(EntityManagerEvent $event)\n {\n if (!$this->supports($event->getEntity())) {\n return;\n }\n\n // ...\n }\n\n public function onDelete(EntityManagerEvent $event)\n {\n if (!$this->supports($event->getEntity())) {\n return;\n }\n\n // ...\n }\n\n public function onPreCreate(EntityManagerEvent $event)\n {\n if (!$this->supports($event->getEntity())) {\n return;\n }\n\n // ...\n }\n\n public function onPreUpdate(EntityManagerEvent $event)\n {\n if (!$this->supports($event->getEntity())) {\n return;\n }\n\n // ...\n }\n\n public function onPreDelete(EntityManagerEvent $event)\n {\n if (!$this->supports($event->getEntity())) {\n return;\n }\n\n // ...\n }\n}\n
"},{"location":"entities/factory/","title":"Factory","text":"Each entity should have a factory that helps to generate a new entity. A factory must implements App\\Core\\Factory\\FactoryInterface.
"},{"location":"entities/factory/#generation","title":"Generation","text":"A factory is basically a service which contain at lease a method named create.
The generation is performed in CLI. These information are required:
- The name of the futur factory (eg:
MyEntityFactory) - The namespace of the entity (eg:
MyEntity)
Simply run php bin/console make:factory.
"},{"location":"entities/factory/#usage","title":"Usage","text":"src/Controller/FooController.phpnamespace App\\Controller;\n\nuse App\\Factory\\MyEntityFactory;\nuse Symfony\\Component\\HttpFoundation\\Response;\n\nclass FooController\n{\n public function foo(MyEntityFactory $factory): Response\n {\n $entity = $factory->create();\n\n // ...\n }\n}\n
"},{"location":"entities/query/","title":"Repository Query","text":"A Repository query is an abstraction of the doctrine repository.
"},{"location":"entities/query/#requirement","title":"Requirement","text":"Entities must implements App\\Core\\Entity\\EntityInterface.
src/Entity/MyEntity.phpnamespace App\\Entity;\n\nuse App\\Repository\\MyEntityRepository;\nuse Doctrine\\ORM\\Mapping as ORM;\nuse App\\Core\\Entity\\EntityInterface;\n\n#[ORM\\Entity(repositoryClass: MyEntityRepository::class)]\nclass MyEntity implements EntityInterface\n{\n // ...\n}\n
"},{"location":"entities/query/#generation","title":"Generation","text":"The generation is performed in CLI. These information are required:
- The namespace of the repository (eg:
MyEntityRepository)
Simply run php bin/console make:repository-query.
Each entity has its own repository query which is a service.
src/Repository/MyEntityRepositoryQuerynamespace App\\Repository;\n\nuse App\\Core\\Repository\\RepositoryQuery;\nuse Knp\\Component\\Pager\\PaginatorInterface;\n\nclass MyEntityRepositoryQuery extends RepositoryQuery\n{\n public function __construct(MyEntityRepository $repository, PaginatorInterface $paginator)\n {\n parent::__construct($repository, 'm', $paginator);\n }\n\n // Example of custom filter\n public function filterByFooBar(bool $foo, bool $bar): self\n {\n $this\n ->andWhere('.foo = :foo')\n ->andWhere('.bar = :bar')\n ->setParameter(':foo', $foo)\n ->setParameter(':bar', $bar);\n\n return $this;\n }\n}\n
"},{"location":"entities/query/#usage","title":"Usage","text":"You are able to find entities in an easy way, without knowing the identification variable and without creating a query builder.
src/Controller/FooController.phpnamespace App\\Controller;\n\nuse App\\Repository\\MyEntityRepositoryQuery\nuse Symfony\\Component\\HttpFoundation\\Response;\n\nclass FooController\n{\n public function foo(MyEntityRepositoryQuery $query): Response\n {\n $entities = $query->create()->find();\n $entity = $query->create()->findOne();\n\n // Filtered and sorted entities\n $entities = $query->create()\n ->orderBy('.id', 'DESC')\n ->where('.isPublished = true')\n ->find();\n\n // ...\n }\n}\n
"},{"location":"entities/query/#custom-methods","title":"Custom methods","text":"// ...\n\nclass MyEntityRepositoryQuery extends RepositoryQuery\n{\n // ...\n\n public function filterByFooBar(bool $foo, bool $bar): self\n {\n $this\n ->andWhere('.foo = :foo')\n ->andWhere('.bar = :bar')\n ->setParameter(':foo', $foo)\n ->setParameter(':bar', $bar);\n\n return $this;\n }\n}\n
$entities = $query->create()\n ->filterByFoo($foo, $bar)\n ->find();\n
In the context of a CRUD, filters are applied using the method useFilters. Integers, strings and booleans are automatically processed. Other types are passed to the method filterHandler.
You have to override it to manage them, example:
// ...\n\nuse App\\Entity\\Something;\n\nclass MyEntityRepositoryQuery extends RepositoryQuery\n{\n // ...\n\n public function filterHandler(string $name, $value): self\n {\n if ($name === 'something' && $value instanceof Something) {\n $this\n ->join('something', 's')\n ->andWhere('s.id = :something')\n ->setParameter('something', $value->getId())\n ;\n }\n }\n}\n
You can also force filterHandler te be used for specific filter field:
// ...\n\nclass MyEntityRepositoryQuery extends RepositoryQuery\n{\n public function __construct(Repository $repository, PaginatorInterface $paginator)\n {\n // ...\n\n $this->addForcedFilterHandler('foo');\n }\n\n public function filterHandler(string $name, $value): self\n {\n // ...\n\n if ($name === 'foo) {\n // ...\n }\n }\n\n}\n
"},{"location":"entities/query/#pager","title":"Pager","text":"You can paginate entities (Knp\\Component\\Pager\\Pagination\\PaginationInterface):
$pager = $query->create()->paginate($page, $maxPerPage);\n
"},{"location":"settings/global/","title":"Global settings","text":""},{"location":"settings/global/#create-settings","title":"Create settings","text":"The creation of settings is based on events.
Using an event subscriber, you can create settings and define how to edit them. A setting's value is stored in json so a value could be a string, a boolean, an array, etc.
See the example below.
src/EventSubscriber/SettingEventSubscriber.phpnamespace App\\EventSubscriber;\n\nuse App\\Core\\Event\\Setting\\SettingEvent;\nuse App\\Core\\EventSubscriber\\SettingEventSubscriber as EventSubscriber;\nuse App\\Core\\Setting\\SettingManager;\nuse Symfony\\Component\\Form\\Extension\\Core\\Type\\CheckboxType;\nuse Symfony\\Component\\Form\\Extension\\Core\\Type\\ColorType;\n\nclass SettingEventSubscriber extends EventSubscriber\n{\n protected SettingManager $manager;\n\n public function __construct(SettingManager $manager)\n {\n $this->manager = $manager;\n }\n\n public function onInit(SettingEvent $event)\n {\n $this->manager->init('app_font_color', 'Design', 'Font color', , '#fff');\n $this->manager->init('app_background_color', 'Design', 'Background color', '#333');\n $this->manager->init('app_maintenance_enabled', 'System', 'Maintenance', false);\n }\n\n public function onFormInit(SettingEvent $event)\n {\n $data = $event->getData();\n $builder = $data['builder'];\n $entity = $data['entity'];\n\n if (in_array($entity->getCode(), ['app_font_color', 'app_background_color'])) {\n $builder->add(\n 'value',\n ColorType::class,\n [\n 'label' => $entity->getLabel(),\n ]\n );\n }\n\n if (in_array($entity->getCode(), ['app_maintenance_enabled'])) {\n $builder->add(\n 'value',\n CheckboxType::class,\n [\n 'label' => $entity->getLabel(),\n ]\n );\n }\n }\n}\n
Result:
"},{"location":"settings/global/#access-settings","title":"Access settings","text":"Settings are accessible using App\\Core\\Setting\\SettingManager which is a service.
src/Controller/FooController.phpnamespace App\\Controller;\n\nuse App\\Core\\Setting\\SettingManager;\nuse Symfony\\Component\\HttpFoundation\\Response;\n\nclass FooController\n{\n public function foo(SettingManager $settingManager): Response\n {\n $fontColor = $settingManager->get('app_font_color');\n $backgroundColor = $settingManager->get('app_background_color');\n $maintenanceEnabled = $settingManager->get('app_maintenance_enabled');\n\n // ...\n }\n}\n
In a template, you can use the function setting:
Font color: {{ setting('app_font_color') }}<br>\nBackground color: {{ setting('app_background_color') }}<br>\nMaintenance enabled: {{ setting('app_maintenance_enabled') ? 'Yes' : 'No' }}<br>\n
"},{"location":"settings/global/#update-settings","title":"Update settings","text":"Settings are accessible using App\\Core\\Setting\\SettingManager which is a service.
src/Controller/FooController.phpnamespace App\\Controller;\n\nuse App\\Core\\Setting\\SettingManager;\nuse Symfony\\Component\\HttpFoundation\\Response;\n\nclass FooController\n{\n public function foo(SettingManager $settingManager): Response\n {\n $settingManager->set('app_font_color', '#f00');\n $settingManager->set('app_background_color', '#00f');\n $settingManager->set('app_maintenance_enabled', true);\n\n // ...\n }\n}\n
You can also edit them from UI:
"},{"location":"settings/global/#options","title":"Options","text":"You can add options using this way:
$event->setOption('view', 'large');\n
Available options:
view (default: false): show a large modal
"},{"location":"settings/navigation/","title":"Navigation settings","text":""},{"location":"settings/navigation/#create-settings","title":"Create settings","text":"The creation of settings is based on events.
Using an event subscriber, you can create settings and define how to edit them. A setting's value is stored in json so a value could be a string, a boolean, an array, etc.
See the example below.
src/EventSubscriber/NavigationSettingEventSubscriber.php
namespace App\\EventSubscriber;\n\nuse App\\Core\\Event\\Setting\\NavigationSettingEvent;\nuse App\\Core\\EventSubscriber\\NavigationSettingEventSubscriber as EventSubscriber;\nuse App\\Core\\Setting\\NavigationSettingManager;\nuse Symfony\\Component\\Form\\Extension\\Core\\Type\\TextType;\nuse Symfony\\Component\\Form\\Extension\\Core\\Type\\EmailType;\n\nclass NavigationSettingEventSubscriber extends EventSubscriber\n{\n protected NavigationSettingManager $manager;\n\n public function __construct(NavigationSettingManager $manager)\n {\n $this->manager = $manager;\n }\n\n public function onInit(NavigationSettingEvent $event)\n {\n $data = $event->getData();\n $navigation = $data['navigation'];\n\n $this->manager->init($navigation, 'nav_tracker_code', 'Stats', 'Tracker', '');\n $this->manager->init($navigation, 'nav_contact_email', 'Contact', 'Email', 'foo@example.com');\n }\n\n public function onFormInit(NavigationSettingEvent $event)\n {\n $data = $event->getData();\n $builder = $data['builder'];\n $entity = $data['entity'];\n\n if (in_array($entity->getCode(), ['nav_tracker_code'])) {\n $builder->add(\n 'value',\n TextType::class,\n [\n 'label' => $entity->getLabel(),\n ]\n );\n }\n\n if (in_array($entity->getCode(), ['nav_contact_email'])) {\n $builder->add(\n 'value',\n EmailType::class,\n [\n 'label' => $entity->getLabel(),\n ]\n );\n }\n }\n}\n
Result: "},{"location":"settings/navigation/#access-settings","title":"Access settings","text":"Settings are accessible using App\\Core\\Setting\\NavigationSettingManager which is a service.
src/Controller/FooController.phpnamespace App\\Controller;\n\nuse App\\Core\\Setting\\NavigationSettingManager;\nuse App\\Core\\Site\\SiteRequest;\nuse Symfony\\Component\\HttpFoundation\\Response;\n\nclass FooController\n{\n public function foo(NavigationSettingManager $settingManager, SiteRequest $siteRequest): Response\n {\n $trackerCode = $settingManager->get($siteRequest->getNavigation(), 'nav_tracker_code');\n $contactEmail = $settingManager->get('my_nav', 'nav_contact_email');\n\n // ...\n }\n}\n
In a template, you can use the function navigation_setting:
Tracker code: {{ navigation_setting(_navigation, 'nav_tracker_code') }}<br>\nContact email: {{ navigation_setting('my_nav', 'nav_contact_email') }}<br>\n
"},{"location":"settings/navigation/#update-settings","title":"Update settings","text":"Settings are accessible using App\\Core\\Setting\\NavigationSettingManager which is a service.
src/Controller/FooController.phpnamespace App\\Controller;\n\nuse App\\Core\\Setting\\NavigationSettingManager;\nuse App\\Core\\Site\\SiteRequest;\nuse Symfony\\Component\\HttpFoundation\\Response;\n\nclass FooController\n{\n public function foo(NavigationSettingManager $settingManager, SiteRequest $siteRequest): Response\n {\n $settingManager->set($siteRequest->getNavigation(), 'nav_tracker_code', '...');\n $settingManager->set('my_nav', 'nav_contact_email', '...');\n\n // ...\n }\n}\n
You can also edit them from UI:
"},{"location":"tree/","title":"Tree manager","text":"Murph manages contents this way:
- You define navigations
- For each navigation, you define menus
- For each menu, you define nodes (elements)
- and for each node, you define:
- an optional page
- the routing
- some attributes
- a sitemap configuration
"},{"location":"tree/#diagram","title":"Diagram","text":"%%{\n init: {\n \"theme\": \"dark\",\n \"flowchart\": {\n \"curve\": \"cardinal\"\n }\n }\n}%%\n\ngraph TB\n N1[Navigation 1] --> M1[Menu 1];\n N2[Navigation ...];\n NX[Navigation N];\n\n N1 --> M2[Menu ...];\n N1 --> MX[Menu N];\n\n N2 --> L1[...]\n NX --> L2[...]\n\n M1 --> MN1[Node 1]\n M1 --> MN2[Node ...]\n M1 --> MN3[Node N]\n\n M2 --> L3[...]\n MX --> L4[...]\n\n MN1 --> P1[Page]\n\n P1 --> B1[Block 1]\n P1 --> B2[Block ...]\n P1 --> BN[Block N]
"},{"location":"tree/menu/","title":"Menu","text":"To create a menu, go to Trees, select the navigation and click on Add a menu. Then fill the form and save.
- The
label is the label displayed whenever necessary (eg: Top menu) - The
code is an unique technical identifier (in the given navigation) and it is useful in templating, routing and settings (eg: top)
When a menu is created then an node is automatically generated.
"},{"location":"tree/navigation/","title":"Navigation","text":"To create a navigation, go to Navigations and click on New. Then fill the form and save.
- The
label is the label displayed whenever necessary (eg: Example) - The
locale is the language used in the content (eg: en) - The
code is a unique technical identifier useful in templating, routing and settings (eg: example_en) - The
domain defines the main domain used to access the navigation (eg: example.com) Additional domains are additional domains used to access the navigation (eg: www.example.com). You can specify regular expression to match all that you want
If several navigations share the same domain, then the locale will by used to prefix routes. But if a navigation uses a single domain then the local will not prefix routes.
"},{"location":"tree/node/","title":"Node","text":"A node allows you to create a tree structure in a menu. To create or update a node, click on \"Edit\" or the sign \"+\". The basic information to fill is label but more parameters are accessible via 4 tabs:
- Content
- Routing
- Attributes
- Sitemap
"},{"location":"tree/node/#content","title":"Content","text":"The content tab allow you to define an optional Page to associate to the node. You can also define that the node is an alias of another node.
"},{"location":"tree/node/#routing","title":"Routing","text":"The routing tab is very important. It allows you to define all parameters related to:
- The routing (obviously):
- An optional URL with parameters
- A route name generated using the field
code
- A custom content-type of the response (eg: 'text/plain')
- A custom controller called when the node is requested
To add a controller in the list, edit config/packages/app.yaml:
core:\n site:\n controllers:\n - {name: 'Foo', action: 'App\\Controller\\ExampleController::foo'}\n - {name: 'Bar', action: 'App\\Controller\\OtherController::bar'}\n
If you need to restrict the access, you can provided a list of roles in the configuration:
config/packages/app.yamlcore:\n site:\n security:\n roles:\n - {name: 'Foo role', role: 'ROLE_FOO'}\n - {name: 'Bar role', role: 'ROLE_BAR'}\n
Then you will be able to select what roles are required:
"},{"location":"tree/node/#attributes","title":"Attributes","text":"Attributes are a collection of keys and values attached to a node (eg: class, icon, whatever you want).
"},{"location":"tree/node/#sitemap","title":"Sitemap","text":"This tab contains information to configure the sitemap.
"},{"location":"tree/page/","title":"Page","text":"A page is a doctrine entity that contains blocks and form builder. You can run php bin/console make:page and generate a new page in an interactive way.
src/Entity/Page/YourPage.phpnamespace App\\Entity\\Page;\n\nuse App\\Core\\Entity\\Site\\Page\\Block;\nuse App\\Core\\Entity\\Site\\Page\\Page;\nuse App\\Core\\Form\\Site\\Page\\TextBlockType;\nuse Doctrine\\ORM\\Mapping as ORM;\nuse Symfony\\Component\\Form\\FormBuilderInterface;\n\n#[ORM\\Entity]\nclass YourPage extends Page\n{\n public function buildForm(FormBuilderInterface $builder, array $options)\n {\n $builder->add(\n 'myBlock',\n TextBlockType::class,\n [\n 'label' => 'My block',\n 'options' => [\n // options given to the sub form\n ],\n ]\n );\n\n // ...\n }\n\n public function setMyBlock(Block $block)\n {\n return $this->setBlock($block);\n }\n\n public function getMyBlock(): Block\n {\n return $this->getBlock('myBlock');\n }\n\n // ...\n}\n
Then edit config/packages/app.yaml and add your page:
core:\n site:\n pages:\n App\\Entity\\Page\\SimplePage:\n name: 'Simple page'\n templates:\n - {name: \"Default\", file: \"page/simple/default.html.twig\"}\n App\\Entity\\Page\\YourPage:\n name: 'Your page'\n templates:\n - {name: \"Template 1\", file: \"page/your_page/template1.html.twig\"}\n - {name: \"Template 2\", file: \"page/your_page/template2.html.twig\"}\n
"},{"location":"tree/page/#blocks","title":"Blocks","text":""},{"location":"tree/page/#textblocktype","title":"TextBlockType","text":"App\\Core\\Form\\Site\\Page\\TextBlockType will render a symfony TextType.
"},{"location":"tree/page/#textareablocktype","title":"TextareaBlockType","text":"App\\Core\\Form\\Site\\Page\\TextareaBlockType will render a symfony TextareaType.
"},{"location":"tree/page/#choiceblocktype","title":"ChoiceBlockType","text":"App\\Core\\Form\\Site\\Page\\ChoiceBlockType will render a symfony ChoiceType.
"},{"location":"tree/page/#fileblocktype","title":"FileBlockType","text":"App\\Core\\Form\\Site\\Page\\FileBlockType will render a symfony FileType with a download link.
In the getter, you must specify the block:
use App\\Core\\Entity\\Site\\Page\\FileBlock;\n\npublic function getMyBlock(): Block\n{\n return $this->getBlock('myBlock', FileBlock::class);\n}\n
"},{"location":"tree/page/#filepickerblocktype","title":"FilePickerBlockType","text":"App\\Core\\Form\\Site\\Page\\FilePickerBlockType will render a specific widget that use the file manager.
"},{"location":"tree/page/#editorjstextareablocktype","title":"EditorJsTextareaBlockType","text":"App\\Core\\Form\\Site\\Page\\EditorJsTextareaBlockType will render a EditorJs widget.
"},{"location":"tree/page/#grapesjsblocktype","title":"GrapesJsBlockType","text":"App\\Core\\Form\\Site\\Page\\GrapesJsBlockType will render a GrapesJS editor.
"},{"location":"tree/page/#tinymcetextareablocktype","title":"TinymceTextareaBlockType","text":"App\\Core\\Form\\Site\\Page\\TinymceTextareaBlockType will render a Tinymce editor.
"},{"location":"tree/page/#imageblocktype","title":"ImageBlockType","text":"App\\Core\\Form\\Site\\Page\\ImageBlockType will render a symfony FileType with a preview of the image.
In the getter, you must specify the block:
use App\\Core\\Entity\\Site\\Page\\FileBlock;\n\npublic function getMyBlock(): Block\n{\n return $this->getBlock('myBlock', FileBlock::class);\n}\n
"},{"location":"tree/page/#collectionblocktype","title":"CollectionBlockType","text":"App\\Core\\Form\\Site\\Page\\CollectionBlockType will a render a symfony CollectionType with availabity to add and remove elements.
use App\\Core\\Entity\\Site\\Page\\CollectionBlock;\n\npublic function getMyBlock(): Block\n{\n return $this->getBlock('myBlock', CollectionBlock::class);\n}\n
"},{"location":"tree/page/#event","title":"Event","text":"When a page is being edited, the options can be set as follows:
src/EventSubscriber/PageEventSubscriber.phpnamespace App\\EventSubscriber;\n\nuse App\\Core\\Event\\Page\\PageEditEvent;\nuse App\\Entity\\Page\\YourPage;\n\nclass PageEventSubscriber implements EventSubscriberInterface\n{\n public static function getSubscribedEvents()\n {\n return [\n PageEditEvent::FORM_INIT_EVENT => ['onFormInit'],\n ];\n }\n\n public function onFormInit(PageEditEvent $event)\n {\n if ($event->getPage() instanceof YourPage) {\n $event->addPageBuilderOptions([\n // options\n ]);\n }\n }\n}\n
"},{"location":"utils/cache/","title":"Cache Manager","text":" - Service:
App\\Core\\Cache\\SymfonyCacheManager - Methods:
cleanRouting(): clear the cache of routes cleanAll(OutputInterface $output = null): clean and warmup all cache
"},{"location":"utils/doctrine/","title":"Doctrine","text":""},{"location":"utils/doctrine/#timestampable","title":"Timestampable","text":"App\\Core\\Doctrine\\Timestampable is a trait usuble in an entity. It adds createdAt and updatedAt datetime attributes with the setters and the getters :
setCreatedAt(?\\DateTime $createdAt): self setUpdated(?\\DateTime $createdAt): self getCreatedAt(): ?\\DateTime getUpdatedAt(): ?\\DateTime
When the entity is created or updated, createdAt and updatedAt are automatically updated to.
"},{"location":"utils/doctrine/#usage","title":"Usage","text":"src/Entity/FooEntity.phpnamespace App/Entity;\n\nuse use App\\Core\\Entity\\EntityInterface;\nuse Doctrine\\ORM\\Mapping as ORM;\n\nuse App\\Core\\Doctrine\\Timestampable;\nuse App\\Core\\Entity\\EntityInterface;\nuse App\\Repository\\FooRepository;\nuse Doctrine\\ORM\\Mapping as ORM;\n\n#[ORM\\Entity(repositoryClass: FooRepository::class)]\n#[ORM\\HasLifecycleCallbacks]\nclass FooEntity implements EntityInterface\n{\n use Timestampable;\n\n // ...\n}\n
"},{"location":"utils/file_attribute/","title":"File attribute","text":"App\\Core\\File\\FileAttribute::handleFile transforms a file path to an instance of Symfony\\Component\\HttpFoundation\\File\\File. You can specify another class if needed. If the path is null or if the file does not exist, it returns null.
use App\\Core\\File\\FileAttribute;\nuse App\\Foo\\Bar;\n\n$path = 'path/to/file';\n$file = FileAttribute::handleFile($path); // returns an instance of File\n\n$path = 'path/to/file';\n$file = FileAttribute::handleFile($path, Bar::class); // returns an instance of Bar\n\n$path = 'path/to/removed_file';\n$file = FileAttribute::handleFile($path); // returns null\n\n$path = null;\n$file = FileAttribute::handleFile($path); // returns null\n
"},{"location":"utils/file_handler/","title":"File upload handler","text":"App\\Core\\Form\\FileUploadHandler is a service and helps you to upload a file. See example below.
use App\\Core\\Form\\FileUploadHandler;\nuse App\\Entity\\Foo;\nuse App\\Form\\FooType;\nuse Symfony\\Component\\HttpFoundation\\Request;\n\npublic function upload(Request $request, FileUploadHandler $fileUpload)\n{\n $entity = new Foo();\n $form = $this->createForm(FooType::class, $foo);\n\n if ($request->isMethod('POST')) {\n $form->handleRequest($request);\n\n if ($form->isValid()) {\n $fileDirectory = 'uploads/';\n $keepOriginalFilename = false;\n\n // single file\n $fileUpload->handleForm(\n uploadedFile: $form->get('image')->getData(), // Symfony\\Component\\HttpFoundation\\File\\UploadedFile or null\n path: $fileDirectory,\n // optional\n afterUploadCallback: function ($filename) use ($entity, $fileDirectory) {\n $entity->setImage($fileDirectory.$filename);\n },\n // optional\n keepOriginalFilename: $keepOriginalFilename\n );\n\n // multiple files\n $fileUpload->handleForm(\n uploadedFile: $form->get('images')->getData(), // Symfony\\Component\\HttpFoundation\\File\\UploadedFile or null\n path: $fileDirectory,\n // optional\n afterUploadCallback: function ($filename) use ($entity, $fileDirectory) {\n $entity->addImage($fileDirectory.$filename);\n },\n // optional\n afterUploadsCallback: function (array $filenames) use ($entity, $fileDirectory) {\n foreach ($filenames as $filename) {\n $entity->addImage($fileDirectory.$filename);\n }\n },\n // optional\n keepOriginalFilename: $keepOriginalFilename\n );\n\n\n // ...\n }\n }\n}\n
If you need to generate custom filenames, FileUploadHandler allows you to define a generator:
use Symfony\\Component\\HttpFoundation\\File\\UploadedFile;\n\n$fileUpload->setFilenameGenerator(function(UploadedFile $file) {\n return sprintf('%d.%s', mt_rand(), $file->guessExtension());\n});\n
"},{"location":"utils/mail/","title":"Mail","text":"App\\Core\\Notification\\MailNotifier is a service and helps you to create a mail using twig template.
Useful methods:
setRecipients(array $recipients): MailNotifier setBccRecipients(array $bccRecipients): MailNotifier setSubject(?string $subject): MailNotifier setFrom($from): MailNotifier setReplyTo($replyTo): MailNotifier setAttachments(array $attachments): MailNotifier addRecipient(string $email, bool $isBcc = false): MailNotifier addRecipients(array $emails, bool $isBcc = false): MailNotifier addRecipientByUser(\\App\\Entity\\User $user, bool $isBcc = false): self addRecipientsByUsers($users, bool $isBcc = false): self addAttachment(string $attachment): MailNotifier addAttachments(array $attachments): MailNotifier init(): MailNotifier notify(string $template, array $data = [], string $type = 'text/html'): MailNotifier
Exemple:
use App\\Core\\Notification\\MailNotifier;\nuse App\\Repository\\UserRepositoryQuery;\n\npublic function foo(MailNotifier $notifier, UserRepositoryQuery $query)\n{\n // ...\n\n $notifier\n ->init()\n ->setSubject('Your bill')\n ->addRecipient('john.doe@example.com')\n ->addRecipients($query->create()->where('.isAdmin = true')->find(), true)\n ->addAttachment('path/to/bill.pdf')\n ->notify('mail/bill.html.twig', [\n // view params\n ])\n ;\n}\n
"},{"location":"utils/slug/","title":"Slug","text":"Murph requires cocur/slugify. See the official documentation on Github.
"},{"location":"utils/editors/builder/","title":"Builder Block","text":"Its gives you total control to build with blocks.
"},{"location":"utils/editors/builder/#add-the-builder-in-forms","title":"Add the builder in forms","text":""},{"location":"utils/editors/builder/#classic-form","title":"Classic form","text":"src/Form/ExampleType.phpnamespace App\\Form\\ExampleType;\n\nuse App\\Core\\Form\\Type\\BuilderType;\nuse Symfony\\Component\\Form\\AbstractType;\nuse Symfony\\Component\\Form\\FormBuilderInterface;\n\nclass ExampleType extends AbstractType\n{\n public function buildForm(FormBuilderInterface $builder, array $options)\n {\n $builder->add(\n 'myField',\n BuilderType::class\n );\n\n // ...\n }\n\n // ...\n}\n
"},{"location":"utils/editors/builder/#page-form","title":"Page form","text":"src/Entity/Page/YourPage.phpnamespace App\\Entity\\Page;\n\nuse App\\Core\\Entity\\Site\\Page\\Block;\nuse App\\Core\\Entity\\Site\\Page\\BuilderBlock;\nuse App\\Core\\Form\\Site\\Page\\BuilderBlockType;\nuse Symfony\\Component\\Form\\FormBuilderInterface;\n\n#[ORM\\Entity]\nclass YourPage extends Page\n{\n public function buildForm(FormBuilderInterface $builder, array $options)\n {\n $builder->add(\n 'myBlock',\n BuilderBlockType::class,\n [\n 'label' => 'My block',\n 'row_attr' => [\n ],\n 'options' => [\n // options given to the sub form\n ],\n ]\n );\n\n // ...\n }\n\n public function setMyBlock(Block $block)\n {\n return $this->setBlock($block);\n }\n\n public function getMyBlock(): Block\n {\n return $this->getBlock('myBlock', BuilderBlock::class);\n }\n\n // ...\n}\n
Create a template:
templates/page/your_page/default.html.twig{% extends 'base.html.twig' %}\n\n{% block page %}\n {{ _page.myBlock.value|block_to_html }}\n{% endblock %}\n
"},{"location":"utils/editors/builder/#creating-custom-block","title":"Creating custom block","text":"The easy way is to run: php bin/console make:builder-block.
First, create a service which extends App\\Core\\BuilderBlock\\BuilderBlock and tagged builder_block.widget. Then, implement the method configure as below.
src/BuilderBlock/CustomBlock.phpnamespace App\\BuilderBlock;\n\nuse App\\Core\\BuilderBlock\\BuilderBlock;\nuse Symfony\\Component\\DependencyInjection\\Attribute\\AutoconfigureTag;\n\n#[AutoconfigureTag('builder_block.widget')]\nclass CustomBlock extends BuilderBlock\n{\n public function configure()\n {\n $this\n ->setName('custom')\n ->setCategory('Category')\n ->setLabel('My custom block')\n ->setOrder(1)\n ->setIsContainer(false) // set `true` if the block can contain blocks\n ->setIcon('<i class=\"fas fa-pencil-alt\"></i>')\n ->setTemplate('builder_block/custom.html.twig')\n ->setClass('col-md-12')\n ->addSetting(name: 'value', label: 'Value', type: 'textarea', attributes: [], default: 'Default value')\n ;\n }\n}\n
Create a template:
templates/builder_block/custom.html.twig<div id=\"{{ id }}\">\n {{ settings.value|default(null) }}\n\n {# If it's a container: #}\n {% for item in children %}\n {{ item|block_to_html(context) }}\n {% endfor %}\n</div>\n
That's all folks!
"},{"location":"utils/editors/builder/#rendering","title":"Rendering","text":"To render blocks, simply use {{ value|block_to_html }}.
If you need to build variables depending of the content, you can override the method buildVars:
src/BuilderBlock/CustomBlock.phpnamespace App\\BuilderBlock;\n\nuse App\\Core\\BuilderBlock\\BuilderBlock;\nuse Symfony\\Component\\DependencyInjection\\Attribute\\AutoconfigureTag;\n\n#[AutoconfigureTag('builder_block.widget')]\nclass CustomBlock extends BuilderBlock\n{\n // ...\n\n public function buildVars(array $data, array $context)\n {\n $this->vars['bar'] = 'bar';\n }\n}\n
And you can access variables in the template:
templates/builder_block/custom.html.twig{{ vars.bar }}\n
"},{"location":"utils/editors/editorjs/","title":"Editor.js","text":"Workspace in classic editors is made of a single contenteditable element, used to create different HTML markups. Editor.js workspace consists of separate Blocks: paragraphs, headings, images, lists, quotes, etc. Each of them is an independent contenteditable element (or more complex structure) provided by Plugin and united by Editor's Core.
Editor.js is fully integrated in Murph as form types.
"},{"location":"utils/editors/editorjs/#classic-form","title":"Classic form","text":"// src/Form/ExampleType.php\nnamespace App\\Form\\ExampleType;\n\nuse App\\Core\\Form\\Type\\EditorJsTextareaType;\nuse Symfony\\Component\\Form\\AbstractType;\nuse Symfony\\Component\\Form\\FormBuilderInterface;\n\nclass ExampleType extends AbstractType\n{\n public function buildForm(FormBuilderInterface $builder, array $options)\n {\n $builder->add(\n 'myField',\n EditorJsTextareaType::class\n );\n\n // ...\n }\n\n // ...\n}\n
Modified data should return stringified JSON array if empty:
public function getMyField(): string\n{\n if (empty($this->myField)) {\n $this->myField = '[]';\n }\n\n return $this->myField;\n}\n
"},{"location":"utils/editors/editorjs/#page-form","title":"Page form","text":"// src/Entity/Page/YourPage.php\nnamespace App\\Entity\\Page;\n\nuse App\\Core\\Entity\\Site\\Page\\Block;\nuse App\\Core\\Form\\Site\\Page\\EditorJsTextareaBlockType;\n\n#[@ORM\\Entity]\nclass YourPage extends Page\n{\n public function buildForm(FormBuilderInterface $builder, array $options)\n {\n $builder->add(\n 'myBlock',\n EditorJsTextareaBlockType::class,\n [\n 'label' => 'My block',\n 'row_attr' => [\n ],\n 'options' => [\n // options given to the sub form\n ],\n ]\n );\n\n // ...\n }\n\n public function setMyBlock(Block $block)\n {\n return $this->setBlock($block);\n }\n\n public function getMyBlock(): Block\n {\n return $this->getBlock('myBlock');\n }\n\n // ...\n}\n
"},{"location":"utils/editors/editorjs/#rendering","title":"Rendering","text":"Editor.js will generate a JSON which contains blocks.
Supported blocks:
- paragraph
- header
- quote
- delimiter
- warning
- list
- nestedList
- checkList
- table
- code
- raw
- image
- link
To render HTML, the basic way is: {{ value|editorjs_to_html }} If you want to render specific blocks: {{ value|editorjs_to_html(['paragraph', 'header', ...])) }}
Block have default templates stored in vendor/murph/murph-core/src/core/Resources/views/editorjs. They can be simply overridden in config/packages/app.yaml:
core:\n editor_js:\n blocks:\n paragraph: 'path/to/paragraph.html.twig'\n header: 'path/to/header.html.twig'\n
"},{"location":"utils/editors/grapesjs/","title":"GrapesJS","text":"GrapesJS is a web builder which combines different tools and features with the goal to help users to build HTML templates without any knowledge of coding. It's a very good solution to replace the common WYSIWYG editor like TinyMCE.
GrapesJS is fully integrated in Murph as form types.
"},{"location":"utils/editors/grapesjs/#classic-form","title":"Classic form","text":"// src/Form/ExampleType.php\nnamespace App\\Form\\ExampleType;\n\nuse App\\Core\\Form\\Type\\GrapesJsType;\nuse Symfony\\Component\\Form\\AbstractType;\nuse Symfony\\Component\\Form\\FormBuilderInterface;\n\nclass ExampleType extends AbstractType\n{\n public function buildForm(FormBuilderInterface $builder, array $options)\n {\n $builder->add(\n 'myField',\n GrapesJsType::class\n );\n\n // ...\n }\n\n // ...\n}\n
"},{"location":"utils/editors/grapesjs/#page-form","title":"Page form","text":"// src/Entity/Page/YourPage.php\nnamespace App\\Entity\\Page;\n\nuse App\\Core\\Entity\\Site\\Page\\Block;\nuse App\\Core\\Form\\Site\\Page\\GrapesJsBlockType;\n\n#[ORM\\Entity]\nclass YourPage extends Page\n{\n public function buildForm(FormBuilderInterface $builder, array $options)\n {\n $builder->add(\n 'myBlock',\n GrapesJsBlockType::class,\n [\n 'label' => 'My block',\n 'row_attr' => [\n ],\n 'options' => [\n // options given to the sub form\n ],\n ]\n );\n\n // ...\n }\n\n public function setMyBlock(Block $block)\n {\n return $this->setBlock($block);\n }\n\n public function getMyBlock(): Block\n {\n return $this->getBlock('myBlock');\n }\n\n // ...\n}\n
"},{"location":"utils/editors/grapesjs/#options","title":"Options","text":"There are 3 modes:
- Bootstrap 4 (default):
bootstrap4 - Preset webpage:
presetWebpage - Preset newsletter:
presetNewsletter
To specify a mode, you must define the attribute data-grapesjs:
src/Form/ExampleType.php$builder->add(\n 'myField',\n GrapesJsType::class,\n [\n // ...\n\n 'attr' => [\n 'data-grapesjs' => 'bootstrap4',\n ],\n ]\n);\n\n```php-inline title=\"src/Entity/Page/YourPage.php\"\n$builder->add(\n 'myBlock',\n GrapesJsBlockType::class,\n [\n // ...\n\n 'options' => [\n 'attr' => [\n 'data-grapesjs' => 'bootstrap4',\n ],\n ],\n ]\n);\n
"},{"location":"utils/editors/grapesjs/#rendering","title":"Rendering","text":"GrapesJS will generate a JSON which contains HTML and CSS.
- To extract HTML:
{% set html = value|grapesjs_html %} - To extract CSS:
{% set css = value|grapesjs_css %}
Depending of the mode, you will need to import in the app sass file:
- Bootstrap 4:
@import \"~bootstrap/scss/bootstrap.scss\"; - Preset webpage:
@import \"~grapesjs-preset-webpage/dist/grapesjs-preset-webpage.min.css\"; - Preset newsletter:
@import \"~grapesjs-preset-newsletter/dist/grapesjs-preset-newsletter.css\";
"},{"location":"utils/editors/tinymce/","title":"TinyMCE","text":"TinyMCE gives you total control over your rich text editing. It's fully integrated in Murph as form types.
"},{"location":"utils/editors/tinymce/#classic-form","title":"Classic form","text":"src/Form/ExampleType.phpnamespace App\\Form\\ExampleType;\n\nuse App\\Core\\Form\\Type\\TinymceTextareaType;\nuse Symfony\\Component\\Form\\AbstractType;\nuse Symfony\\Component\\Form\\FormBuilderInterface;\n\nclass ExampleType extends AbstractType\n{\n public function buildForm(FormBuilderInterface $builder, array $options)\n {\n $builder->add(\n 'myField',\n TinymceTextareaType::class\n );\n\n // ...\n }\n\n // ...\n}\n
"},{"location":"utils/editors/tinymce/#page-form","title":"Page form","text":"src/Entity/Page/YourPage.phpnamespace App\\Entity\\Page;\n\nuse App\\Core\\Entity\\Site\\Page\\Block;\nuse App\\Core\\Form\\Site\\Page\\TinymceTextareaBlockType;\n\n#[ORM\\Entity]\nclass YourPage extends Page\n{\n public function buildForm(FormBuilderInterface $builder, array $options)\n {\n $builder->add(\n 'myBlock',\n TinymceTextareaBlockType::class,\n [\n 'label' => 'My block',\n 'row_attr' => [\n ],\n 'options' => [\n // options given to the sub form\n ],\n ]\n );\n\n // ...\n }\n\n public function setMyBlock(Block $block)\n {\n return $this->setBlock($block);\n }\n\n public function getMyBlock(): Block\n {\n return $this->getBlock('myBlock');\n }\n\n // ...\n}\n
"},{"location":"utils/editors/tinymce/#options","title":"Options","text":"There are 2 predefined modes:
- Default:
default - Light:
light
To specify a mode, you must define the attribute data-tinymce:
src/Form/ExampleType.php$builder->add(\n 'myField',\n TinymceTextareaType::class,\n [\n // ...\n\n 'attr' => [\n 'data-tinymce' => 'light',\n ],\n ]\n);\n
src/Entity/Page/YourPage.php$builder->add(\n 'myBlock',\n TinymceTextareaBlockType::class,\n [\n // ...\n\n 'options' => [\n 'attr' => [\n 'data-tinymce' => 'light',\n ],\n ],\n ]\n);\n
To custom the editor, see the example below:
assets/js/admin.jsimport '../../vendor/murph/murph-core/src/core/Resources/assets/js/admin.js'\n\nwindow.tinymce.language = 'fr_FR'\n\nwindow.tinymceModes = {\n myCustomMode: {\n plugins: '...',\n menubar: '...',\n toolbar: '...'\n quickbars_selection_toolbar: '...'\n contextmenu: '...'\n templates: [\n {\n title: 'Container',\n description: 'Add a bootstrap container',\n content: '<div class=\"container\"><div class=\"selcontent\"></div></div>'\n }\n // ...\n ],\n content_style: '...'\n }\n}\n
src/Form/ExampleType.php$builder->add(\n 'myField',\n TinymceTextareaType::class,\n [\n // ...\n\n 'attr' => [\n 'data-tinymce' => 'myCustomMode',\n ],\n ]\n);\n
src/Entity/Page/YourPage.php$builder->add(\n 'myBlock',\n TinymceTextareaBlockType::class,\n [\n // ...\n\n 'options' => [\n 'attr' => [\n 'data-tinymce' => 'myCustomMode',\n ],\n ],\n ]\n);\n
"},{"location":"utils/editors/tinymce/#rendering","title":"Rendering","text":"TinyMCE generates HTML. To render, simply use {{ value|raw }}.
"},{"location":"utils/form/collection/","title":"Collection","text":"When you need to manage a collection in a form, you can use App\\Core\\Form\\Type\\CollectionType with these options:
Option Type Default Description collection_name string null Unique name of the collection label_add string Add Unique name of the collection label_delete string Delete Unique name of the collection template_before_item string null A template included before an existing itam of the collection template_after_item string null A template included after an existing itam of the collection ...and and all options of a symfony collection type.
"},{"location":"utils/form/file_picker/","title":"File picker","text":"Murph provides a file picker using the file manager system: App\\Core\\Form\\FileManager\\FilePickerType. It allows you to pick an existing file or upload it. After saving, the file is previewed if possible.
"}]}
\ No newline at end of file
+{"config":{"lang":["en"],"separator":"[\\s\\-]+","pipeline":["stopWordFilter"]},"docs":[{"location":"","title":"Welcome to Murph","text":"Murph is an open-source CMF built on top of Symfony that helps you to build your own CMS with several domains and languages. It comes with a fully implemented and customizable tree manager, a CRUD generator, a 2FA authentication, settings and tasks managers, basic web analytics.
Symfony developers will love build on Murph \ud83d\udcaa End users will be fond of the interface and the powerful tools \ud83d\udc9c
Developed with love by Simon Vieille.
Support: Murph project on Matrix.
Access the demo.
"},{"location":"abtesting/","title":"A/B Testing","text":""},{"location":"abtesting/#overview","title":"Overview","text":"Murph contains a basic tools to create A/B Tests.
The logic of the implement follows this logic:
- A node can be configured to enable A/B Test. You also define a code to set the name of the test
- When Murph receives a request, an event is dispatched and contains an
App\\Core\\Ab\\AbTestInterface object - You must create an event subscriber and set variations defined by
- a name (a string)
- a value (whatever you want)
- a percentage chance (optional)
- Then a variation is picked and saved in user cookies
- Finally, you can retrieve the picked variation PHP side and template side
"},{"location":"abtesting/#configure-the-node","title":"Configure the node","text":"Go the navigation and edit the tested node:
- Enable A/B testing
- Define a code (eg:
example_test)
"},{"location":"abtesting/#the-event-subscriber","title":"The Event Subscriber","text":"The event subscriber helps you to define each variation and the TTL.
src/EventSubscriber/MyAbTestEventSubscriber.phpnamespace App\\EventSubscriber;\n\nuse App\\Core\\EventSubscriber\\AbEventSubscriber as EventSubscriber;\nuse App\\Core\\Event\\Ab\\AbTestEvent;\n\nclass MyAbTestEventSubscriber extends EventSubscriber\n{\n public function onInit(AbTestEvent $event)\n {\n if ($event->getTest()->getName() !== 'example_test') {\n return;\n }\n\n $event->getTest()\n ->addVariation('test_1', 'Value #1', 20) // 20% of chance\n ->addVariation('test_2', 'Value #2', 30) // 30% of chance\n ->addVariation('test_3', 'Value #3', 50) // 50% of chance\n ->setDuration(3600 * 24) // duration of the cookie in seconds\n ;\n }\n\n public function onRun(AbTestEvent $event)\n {\n // executed if a variation is newly picked\n }\n}\n
"},{"location":"abtesting/#the-result","title":"The result","text":"you can retrieve the test and the variation picked in PHP side and in template side.
use App\\Core\\Ab\\AbContainerInterface;\n\npublic function foo(AbContainerInterface $testContainer)\n{\n if ($testContainer->has('example_test')) {\n $test = $testContainer->get('example_test');\n\n $result = $test->getResult(); // eg: \"test_2\"\n $value = $test->getResultValue(); // eg: \"Value #2\"\n\n // ...\n }\n\n // ...\n}\n
{% if ab_test_exists('example_test') %}\n {% set test = ab_test('example_test') %}\n {% set result = ab_test_result('example_test') %}\n {% set value = ab_test_value('example_test') %}\n\n {# ... #}\n{% endif %}\n
"},{"location":"abtesting/#global-ab-test","title":"Global A/B Test","text":"If you need to perform an A/B test everywhere, you need to create a specific listener:
src/EventListener/CustomAbListener.phpnamespace App\\EventListener;\n\nuse App\\Core\\EventListener\\AbListener as EventListener;\nuse Symfony\\Component\\HttpFoundation\\Request;\nuse Symfony\\Component\\HttpKernel\\Event\\RequestEvent;\n\nclass CustomAbListener extends EventListener\n{\n /**\n * {@inheritdoc}\n */\n protected function supports(Request $request): bool\n {\n return true;\n }\n\n /**\n * {@inheritdoc}\n */\n protected function getAbTestCode(): string\n {\n return 'my_global_ab_test_code';\n }\n}\n
CustomAbListener must be registred:
config/services.ymlservices:\n # ...\n\n App\\EventListener\\CustomAbListener;\n tags:\n - { name: kernel.event_listener, event: kernel.request }\n - { name: kernel.event_listener, event: kernel.response }\n
"},{"location":"controller/","title":"Controller","text":""},{"location":"controller/#controller_1","title":"Controller","text":"The default controller of a node is App\\Core\\Controller\\Site\\PageController::show. PageController extends Symfony\\Bundle\\FrameworkBundle\\Controller\\AbstractController and implements very basic features: a Response builder which retrieves the good template and injects variables to the view.
To create a custom controller, do this way:
src/Controller/MyController.phpnamespace App\\Controller;\n\nuse App\\Core\\Controller\\Site\\PageController;\n\nclass MyController extends PageController\n{\n public function myAction()\n {\n if (!$this->siteRequest->getPage()) {\n throw $this->createNotFoundException();\n }\n\n return $this->defaultRender($this->siteRequest->getPage()->getTemplate(), [\n // view datas\n ]);\n }\n}\n
Then edit config/packages/app.yaml and add your controller:
core:\n site:\n controllers:\n - {name: 'My action', action: 'App\\Controller\\MyController::myAction'}\n
"},{"location":"controller/#urlgenerator","title":"UrlGenerator","text":"If your controller represents entities and if the associated node is visible in the sitemap, you can use a App\\Core\\Annotation\\UrlGenerator in annotations and implement a generator. See the example below.
src/UrlGenerator/MyEntityGeneratornamespace App\\UrlGenerator;\n\nuse App\\Core\\Entity\\Site\\Node;\nuse App\\Repository\\MyEntityRepositoryQuery;\nuse Symfony\\Component\\Routing\\Generator\\UrlGeneratorInterface;\n\nclass MyEntityGenerator\n{\n protected MyEntityRepositoryQuery $query;\n protected UrlGeneratorInterface $urlGenerator;\n\n public function __construct(MyEntityRepositoryQuery $query, UrlGeneratorInterface $urlGenerator)\n {\n $this->query = $query;\n $this->urlGenerator = $urlGenerator;\n }\n\n public function myActionGenerator(Node $node, array $options): array\n {\n $entities = $this->query->create()->find();\n\n $urls = [];\n\n foreach ($entities as $entity) {\n $urls[] = $this->urlGenerator->generate(\n $node->getRouteName(),\n [\n 'entity' => $entity->getId(),\n '_domain' => $options['_domain'],\n ],\n UrlGeneratorInterface::ABSOLUTE_URL\n );\n }\n\n return $urls;\n }\n}\n
Then, the have to annotate the controller like this (note: options is optional):
src/Controller/MyController.phpnamespace App\\Controller;\n\nuse App\\Core\\Annotation\\UrlGenerator;\nuse App\\Core\\Controller\\Site\\PageController;\nuse App\\UrlGenerator\\MyEntityGenerator;\n\nclass MyController extends PageController\n{\n #[UrlGenerator(service: MyEntityGenerator::class, method: 'myActionGenerator', options=[])]\n public function myAction(MyEntity $entity)\n {\n // do stuff\n }\n}\n
Finally, update config/services.yaml:
services:\n # ...\n\n App\\UrlGenerator\\MyEntityGenerator:\n public: true\n
"},{"location":"procedure/","title":"Installation","text":""},{"location":"procedure/#setting-up-the-skeleton","title":"Setting up the skeleton","text":"Depending of you environment, PHP and composer could be located in specific paths. Defines theme with environment vars:
export PHP_BIN=/usr/bin/php\nexport COMPOSER_BIN=/usr/bin/composer\nexport NPM_BIN=/usr/bin/npm\nexport YARN_BIN=/usr/bin/yarn\n
Create your project:
\"$COMPOSER_BIN\" create-project murph/murph-skeleton my_project ^1\n
An error occured because of the unconfigured database.
- Copy
.env into .env.local - Edit
.env.local (documentation). Don't forget to set APP_SECRET. - Run
make build
"},{"location":"procedure/#create-an-admin-user","title":"Create an admin user","text":"Run \"$PHP_BIN\" bin/console murph:user:create and answer questions.
"},{"location":"procedure/#configure-a-web-server","title":"Configure a web server","text":"Read the documentation of Symfony to configure a web server.
In case of a local server, you can use the Symfony Local Web Server. Then go to https://127.0.0.1:8000/admin.
"},{"location":"requirements/","title":"Technical Requirements","text":" - Install PHP 8.0 or higher and these PHP extensions (which are installed and enabled by default in most PHP 8 installations): Ctype, iconv, JSON, PCRE, Session, SimpleXML, and Tokenizer;
- Install Composer, which is used to install PHP packages.
- Install NodeJS 16 or higher, Yarn and Webpack
- A created database (MySQL, PostgreSQL or SQLite)
- build-essential to run
make
"},{"location":"sources/","title":"Sources codes","text":"Source codes are accessible on Gitnet:
- Core
- Source code the skeleton
"},{"location":"tasks/","title":"Tasks","text":"Tasks are scripts executabled from UI. The creation of tasks is based on events.
src/EventSubscriber/MyTaskEventSubscriber.phpnamespace App\\EventSubscriber;\n\nuse App\\Core\\Event\\Task\\TaskInitEvent;\nuse App\\Core\\Event\\Task\\TaskRunRequestedEvent;\nuse App\\Core\\EventSubscriber\\Task\\TaskEventSubscriber;\n\nclass MyTaskEventSubscriber extends TaskEventSubscriber\n{\n public function onInit(TaskInitEvent $event)\n {\n $event->addTask('my_task', 'Example', 'My task');\n }\n\n public function onRunRequest(TaskRunRequestedEvent $event)\n {\n if ('my_task' !== $event->getTask()) {\n return;\n }\n\n $event->getOutput()->writeln('My task is started');\n\n // ...\n\n $event->getOutput()->writeln('My task is finished');\n }\n}\n
"},{"location":"template/","title":"Templating","text":""},{"location":"template/#variables","title":"Variables","text":"By default, these variables are given to a CMS view:
- The current node is
_node and its menu is _menu - The current navigation is
_navigation - The current locale is
_locale - The CMS store is
_store
"},{"location":"template/#navigations-and-menus","title":"Navigations and menus","text":" - Retrieve all navigations:
_store.navigations -
Retrieve a navigation by its code: _store.navigation('the_code')
-
Retrieve all navigation menus: _navigation.menus
- Retrieve a menu by its code:
_navigation.menu('the_code') - Retrieve all nodes of a menu:
menu.rootNode.children -
Retrieve visible nodes of a menu: menu.rootNode.children({visible: true})
-
Test if a node is the current one: _store.isActiveNode(node)
- Test if a node is or contains the current one:
_store.isActiveNode(node, true)
"},{"location":"template/#page","title":"Page","text":"You can access a page's blocks this way:
{% set myBlock = _page.myBlock.value %}\n\n{{ myBlock }}\n
"},{"location":"template/#url-and-path","title":"URL and path","text":"Murph has somes twig functions to manage URLs:
"},{"location":"template/#generic-functions","title":"Generic functions","text":" - Same as twig url but catches all exceptions:
{{ safe_url(routeName, options, relative) }} - Same as twig path but catches all exceptions:
{{ safe_path(routeName, options, relative) }}
"},{"location":"template/#node-functions","title":"Node functions","text":" - Generates a URL using a node:
{{ node_url(node, options, relative) }} - Generates a path using a node:
{{ node_path(node, options, relative) }} - Generates a URL using a node and catches all exceptions:
{{ safe_node_url(node, options, relative) }} - Generates a path using a node and catches all exceptions:
{{ safe_node_path(node, options, relative) }}
A node may have a disabled URL:
{% if not node.disableUrl %}\n {% set path = safe_node_path(node) %}\n {% set url = safe_node_url(node) %}\n{% endif %}\n
When the navigation has several domains, you can specify the domain:
{% set path = safe_node_path(node, {_domain: _domain}) %}\n{% set url = safe_node_url(node, {_domain: _domain}) %}\n
"},{"location":"template/#code-functions","title":"Code functions","text":" - Generates a URL using codes:
{{ code_url(menuCode, nodeCode, options, relative) }} - Generates a path using codes:
{{ code_path(menuCode, nodeCode, options, relative) }} - Generates a URL using codes and catches all exceptions:
{{ safe_code_url(menuCode, nodeCode, options, relative) }} - Generates a path using codes and catches all exceptions:
{{ safe_code_path(menuCode, nodeCode, options, relative) }}
"},{"location":"template/#filters","title":"Filters","text":"When a content could contains tags (eg: '{{url://my_route}}), usemurph_url`. See the example below:
Code Output {{ content }} A link to the <a href=\"{{url://contact}}\">contact page</a> {{ content|murph_url }} A link to the <a href=\"https://example.com/contact\">contact page</a>"},{"location":"template/#string-builder","title":"String builder","text":"The string builder builds a string using a format and an object or an array.
Examples:
{{ 'Entity ID is {id}'|build_string(myEntity) }} will output: Entity ID is 42 {{ 'Hello, {user.displayName}!'|build_string(myEntity) }} will output Hello, John doe!
In case of an not accessible property, no exception will be thrown.
"},{"location":"template/#file-attributes","title":"File attributes","text":"Attributes are managed from the file manager. They are accessibles with these filters:
Code Result {{ '<img ... alt=\"{{fattr://hash/alt}}\">'|file_attributes }} <img ... alt=\"Attribute 'alt' of the file with the given hash\"> {{ 'path/to/file'|file_attribute('alt') }} Attribute alt of the given file"},{"location":"users/","title":"Users","text":"Murph provided a basic authentication based on the User entity.
"},{"location":"changelog/core/","title":"Changelog","text":"[v1.26.0] - 2025-03-17 Added
- FileUploadHandler: allow to upload multiple files
- CrudController: allow to add callables after creation, update and delation
Fixed
- fix(crud): use context variable to retrieve the form and the form options
- fix(node): use
false instead of O in query
Changed
- CrudConfiguration::setShowActions: add context when setting this parameter
[v1.25.2] - 2025-02-07 Added
- allow to set
data-* attributes on modal
[v1.25.1] - 2024-05-13 Added
- add drag&drop in the block builder
[v1.25.0] - 2024-05-12 Added
- add block builder widget
- allow to use
window.tinymceModes to add or override tinymce modes - add border color on tinymce editor
Fixed
- fix default crud sort
- fix hidden save button in file manager
- fix template of CrudController (maker)
- fix undefined
window.tinymce.murph
[v1.24.1] - 2024-02-01 Fixed
- update Murph version constant
[v1.24.0] - 2024-01-27 Added
- add CSS class
no-wrap - copy the pager of the CRUD at the bottom of the list
Fixed
- fix an issue with the file manager when editing an item opened in a modal
- fix type casting in slugifier
[v1.23.0] - 2023-11-01 Added
- allow to define templates show before and after a murph collection item
- add global batch actions
- add constraint
Length in forms - add sass classes to mange with of elements
- set searchFields option on jschoice manager (search on labels)
Changed
- refactor services using constructor property promotions
- remove twig in the mail notifier service
- change pills colors
- change border colors of inputs when focused
- change colors on js-choices element
Fixed
- fix regression on crud sorting
- fix test in RepositoryQuery::addForcedFilterHandler
- remove parameter $option on CrudConfiguration::setForm and fix CrudController make template
- fix the aspect of the actions's column in the crud
[v1.22.0] - 2023-09-28 Added
- add new options in BooleanField:
toggle|checkbox_class_when_true and toggle|checkbox_class_when_false - add
count method in repository query - add
addForcedFilterHandler method in repository query - add
inline_form_validation option to validate inline forms with custom algo - add crud sorting parameters in the session
- add flush option in the entity manager on create, update, remove, and persist methods
[1.21.1] - 2023-08-17 Added
- add form error handle in inline edit action and refill the form using the previous request content
- add form error handle in ssettings actions and refill the form using the previous request content
Fixed
- fix tinymce reload when modal is closed and reopened
- fix modal hiding when a file is successfuly uploaded in the file manager
[1.21.0] - 2023-08-11 Added
- allow to use array syntax in string builder filter
- add color property in Navigation
- add badge with navigation color in admin views
- add
default_value option in crud fields - add
display option in BooleanField - add associated nodes in page form
Fixed
- fix routes in the global settings controller
[1.20.0] - 2023-07-27 Added
- enable double click on cruds
- add block class name for the choice type in the page maker
- update file details view on the file manager
- add form options in the crud filter action
- add trans filter in inline form modal title
- add setter to define all fields in a defined context
- add filename generator setter in FileUploadHandler
- add variable for the sidebar size
- add twig block to override defaults actions in crud index template
- add option to remove iterable values and/or specifics keys in the twig toArray function
- add boolean field for CRUD
- add context variable in each controllers to simplify overrides
- core.site.name and core.site.logo are not longer required
- add default templates when a crud is generated
- add boolean 'is_disabled' in the menu item template options
Fixed
- fix filemanager date ordering
- fix maker CrudController template: remove bad pasted code
- fix redirect listener: use boolean instead of integer
- fix responsive of account edit template
- fix collection widget: allow_add/allow_delete and prototype
Changed
- user admin routes are defined in core, custom controller is not required
[1.19.0] - 2023-04-15 Added
- feat(page): forms for metas, opengraph and extra informations can be removed
- feat(navigation): user interface is improved
- feat(file): webp is allowed and shown in form widgets and in file manager details
- feat(file): the file manager now show the size and the modification date of a file
- feat(crud): add option
action in field to add a link to the view page or to the edition page - feat(crud): add option
inline_form in field to configure to edit the data - feat(crud): add
setDoubleClick in the crud configuration
[1.18.0] - 2023-01-13 Added
- feat(dep): add symfony/runtime
- feat(dep): add symfony/flex
Fixed
- fix(crud): allow POST in delete actions
- fix(crud): remove default page value in abstract crud controller
- fix(admin): test site_logo before using it
- fix(ui): update z-index of choices__list--dropdown
[1.17.1] - 2022-12-03 Fixed
- add mising attribute on timestampable (doctrine)
[1.17.0] - 2022-11-19 Fixed
- fix tinymce modal z-index in tox
Changed
- replace annotation with attributes
[1.16.0] - 2022-09-06 Added
- add A/B testing feature
- add cleanup of html string extracted from grapesjs content
Fixed
- fix file block type
Changed
- remove dashboard action from the core
[1.15.0] - 2022-05-09 Added
- CrudConfiguration::setAction can receive a callable instead of a boolean in 'enabled' param
- add grapesjs-component-code-editor and grapesjs-parser-postcss
- hide the backoffice site name when small resolution
- add entity_to_array twig function
- add default field to show in crud configuration
Fixed
- fix the mail notifier
- fix sitemap: navigation with several domains
- fix regression with editorjs: content not loaded
Changed
- change default template to show an entity using
entity_to_array
[1.14.1] - 2022-04-30 Added
- add allowed chars in RouteParameterSlugify and CodeSlugify
- improve sidebar in mobile view
Fixed
- fix creation of new element when a menu is edited
- fix editorjs error when the textarea is empty
[1.14.0] - 2022-04-20 Added
- add grapesjs modes
- add tinymce block type
- add editor types in page maker
- add the page template when the page is generated with the maker
Changed
- replace flag-icon-css with flag-icons
[1.13.0] - 2022-04-17 Added
- add editorjs hyperlink block
- add button to show and hide metas (admin)
- add grapesjs editor
- add editorjs type
Fixed
- fix editorjs inline tools (bold and italic)
Changed
- update editorjs quote block template
[1.12.0] - 2022-03-26 Added
- add page maker command (
make:page) - add CrudConfiguration::getViewData in complement of CrudConfiguration::getViewDatas
- add editorjs link block endpoint
Fixed
- fix issue with empty user-agent in AnalyticListener
Changed
- update editorjs image block view
[1.11.0] - 2022-03-22 Added
- add data-modal-create attribute to force modal to be open in a new container
- add blur when several modals are opened
- add specific form types for Tinymce and EditorJS
Changed
- update file-manager with data-modal-create attribute
[1.10.0] - 2022-03-17 Added
- add url and path generators using code (twig)
Changed
- update node entity constraints
[1.9.2] - 2022-03-14 Fixed
- fix issue with murph version constant and autoloader
[1.9.1] - 2022-03-14 Added
- add murph version in autoload file
Changed
- remove AdminController constructor
[1.9.0] - 2022-03-13 Added
- add murph version in admin ui
Changed
- the core is now installed with composer
[1.8.0] - 2022-03-10 Added
- add security roles in app configuration
- add option to restrict node access to specific roles
Changed
- rename
core/EventSuscriber with core/EventSubscriber
[1.7.3] - 2022-03-06 Added
- add ability to rename file in the file manager
Fixed
- fix user factory
- fix user creation from ui
[1.7.2] - 2022-03-03 Added
- add templates to render sections and items in the admin menu
Fixed
- fix the analytic table when a path is a long
[1.7.1] - 2022-03-01 Added
- add translations
Fixed
- fix missing directories
[1.7.0] - 2022-03-01 Fixed
- fix the analytic referers table when a referer has a long domain
Changed
- upgrade dependencies
- move assets to the core directory
[1.6.0] - 2022-02-28 Added
- add block in field templates to allow override
- merge route params in crud admin redirects
- improve murph:user:create command
Fixed
- fix form namespace prefix in the crud controller maker
- fix date field when the value is empty
- fix crud batch column width
- fix sidebar icon width
- fix cache clear task
Changed
- remove password generation from the user factory
[1.5.0] - 2022-02-25 Added
- add desktop views and mobile views
Changed
- upgrade dependencies
- replace jaybizzle/crawler-detect with matomo/device-detector
[1.4.1] - 2022-02-23 Added
- handle app urls in twig routing filters
Fixed
- fix views in analytics modal
- replace empty path with \"/\" in analytics
Changed
- update default templates
[1.4.0] - 2022-02-21 Added
- add basic analytics
[1.3.0] - 2022-02-19 Added
- add support of regexp with substitution in redirect
- url tags can be used as redirect location
- add builders to replace file information tags and url tags
Fixed
- fix filemanager sorting
- fix batch action setter
[1.2.0] - 2022-02-14 Added
- add sort in file manager
- add redirect manager
Changed
- replace node-sass with sass
[1.1.0] - 2022-02-29 Added
- add directory upload in file manager
Fixed
- fix admin node routing
Changed
- symfony/swiftmailer-bundle is replaced by symfony/mailer
[1.0.1] - 2022-02-25 Fixed
- fix Makefile environment vars (renaming)
- fix composer minimum stability
[1.0.0] - 2022-01-23"},{"location":"changelog/skeleton/","title":"Changelog","text":"[v1.23.0] - 2023-09-28 Changed
- upgrade murph/murph-core
[v1.22.0] - 2023-09-28 Added
- update woodpecker ci base file
Fixed
- fix #1: add UniqueEntity constraint in the User entity
Changed
- upgrade murph/murph-core
[1.21.0] - 2023-08-11 Changed
- upgrade murph/murph-core
[1.20.0] - 2023-07-27 Fixed
- fix collection widget: allow_add/allow_delete and prototype
Added
- add user admin controller and simples views in default files
- add chdir in the console entrypoint
Changed
- upgrade murph/murph-core
[1.19.0] - 2023-04-15 Changed
- upgrade murph/murph-core
[1.18.0] - 2023-01-13 Added
- feat(dep): update dependencies
- feat(update): apply new recipe for phpunit
- feat(update): apply recipes:update doctrine/doctrine-bundle
- feat(update): apply recipes:update doctrine/doctrine-migrations-bundle
- feat(update): apply recipes:update liip/imagine-bundle
- feat(update): apply recipes:update stof/doctrine-extensions-bundle
- feat(update): apply recipes:update symfony/apache-pack
- feat(update): apply recipes:update symfony/console
- feat(update): apply recipes:update symfony/debug-bundle
- feat(update): apply recipes:update symfony/flex
- feat(update): apply recipes:update symfony/mailer
- feat(update): apply recipes:update symfony/framework-bundle
- feat(update): apply recipes:update symfony/monolog-bundle
- feat(update): apply recipes:update symfony/routing
- feat(update): apply recipes:update symfony/security-bundle
- feat(update): apply recipes:update symfony/translation
- feat(update): apply recipes:update symfony/twig-bundle
- feat(update): apply recipes:update symfony/validator
- feat(update): apply recipes:update symfony/web-profiler-bundle
- feat(update): apply recipes:update symfony/webpack-encore-bundle
- feat(update): apply recipes:update scheb/2fa-bundle
Fixed
- fix(config): fix typo in 2fa conf
- fix(config): fix firewall config
[1.17.0] - 2022-11-19 Changed
- upgrade murph/murph-core
- replace annotation with attributes
- use encore from node_modules in npm scripts
[1.16.0] Added
- add a admin dashboard controller
- add meta description in base.html.twig
Fixed
Changed
- upgrade murph/murph-core
[1.15.0] Changed
- upgrade murph/murph-core
[1.14.3] Added
- add blocks in default template
Changed
- upgrade murph/murph-core
[1.14.2] [1.14.1] Fixed
- fix missing envvar in makefile (npm)
[1.14.0] Changed
- upgrade murph/murph-core
[1.13.0] Changed
- upgrade murph/murph-core
[1.12.0] Changed
- upgrade murph/murph-core
[1.11.0] Changed
- upgrade murph/murph-core
- use murph-npm to install npm requirements
[1.10.0] Added
- add translated title in dashboard template
Fixed
- remove useless env var from makefile
Changed
- upgrade murph/murph-core
[1.9.1] - 2022-03-14 Added
- add murph version in autoload file
Changed
- remove AdminController constructor
[1.9.0] - 2022-03-13 Added
- add murph version in admin ui
Changed
- the core is now installed with composer
[1.8.0] - 2022-03-10 Added
- add security roles in app configuration
- add option to restrict node access to specific roles
Changed
- rename
core/EventSuscriber with core/EventSubscriber
[1.7.3] - 2022-03-06 Added
- add ability to rename file in the file manager
Fixed
- fix user factory
- fix user creation from ui
[1.7.2] - 2022-03-03 Added
- add templates to render sections and items in the admin menu
Fixed
- fix the analytic table when a path is a long
[1.7.1] - 2022-03-01 Added
- add translations
Fixed
- fix missing directories
[1.7.0] - 2022-03-01 Fixed
- fix the analytic referers table when a referer has a long domain
Changed
- upgrade dependencies
- move assets to the core directory
[1.6.0] - 2022-02-28 Added
- add block in field templates to allow override
- merge route params in crud admin redirects
- improve murph:user:create command
Fixed
- fix form namespace prefix in the crud controller maker
- fix date field when the value is empty
- fix crud batch column width
- fix sidebar icon width
- fix cache clear task
Changed
- remove password generation from the user factory
[1.5.0] - 2022-02-25 Added
- add desktop views and mobile views
Changed
- upgrade dependencies
- replace jaybizzle/crawler-detect with matomo/device-detector
[1.4.1] - 2022-02-23 Added
- handle app urls in twig routing filters
Fixed
- fix views in analytics modal
- replace empty path with \"/\" in analytics
Changed
- update default templates
[1.4.0] - 2022-02-21 Added
- add basic analytics
[1.3.0] - 2022-02-19 Added
- add support of regexp with substitution in redirect
- url tags can be used as redirect location
- add builders to replace file information tags and url tags
Fixed
- fix filemanager sorting
- fix batch action setter
[1.2.0] - 2022-02-14 Added
- add sort in file manager
- add redirect manager
Changed
- replace node-sass with sass
[1.1.0] - 2022-02-29 Added
- add directory upload in file manager
Fixed
- fix admin node routing
Changed
- symfony/swiftmailer-bundle is replaced by symfony/mailer
[1.0.1] - 2022-02-25 Fixed
- fix Makefile environment vars (renaming)
- fix composer minimum stability
[1.0.0] - 2022-01-23"},{"location":"crud/","title":"CRUD","text":"Murph helps you to manage specific entities with a CRUD manager:
- Create entities
- Read (Show) entities
- Update entities
- Delete entities
You can configure almost anything:
- How to list entities:
- Information to show
- Available actions
- Filters
- Sorting
- ...
- How to show an entity
- How to create and update an entity
"},{"location":"crud/configuration/","title":"Configuration","text":"A generated crud controller contains a method named getConfiguration. This methods returns a instance of App\\Core\\Crud\\CrudConfiguration.
"},{"location":"crud/configuration/#setpagetitle","title":"setPageTitle","text":"setPageTitle(string $page, string $title)
Set the title of the given page.
Example of usage in a CRUD template: <title>{{ configuration.pageTitle('index') }}</title>.
"},{"location":"crud/configuration/#setpageroute","title":"setPageRoute","text":"setPageRoute(string $page, string $route)
Set the route of the given page. By default, pages are: index, edit, new, show. You can create a custom page for a custom controller.
Example of usage in a CRUD template: <a href=\"{{ path(configuration.pageRoute('new')) }}\">...</a>.
"},{"location":"crud/configuration/#setform","title":"setForm","text":"setForm(string $context, string $form,array$options = [])
Set the form used in the given context.
"},{"location":"crud/configuration/#setformoptions","title":"setFormOptions","text":"setFormOptions(string $context,array$options = [])
Defines options given to a form.
"},{"location":"crud/configuration/#setaction","title":"setAction","text":"setAction(string $page, string $action, bool|callable $enabled)
Set if an action is enabled or not in the given page. Take a look at core/Resources/views/admin/crud/*.html.twig for more information. Depending the context, the callable could receive the entity in parameter. Example:
->setAction('index', 'edit', function(EntityInterface $entity) {\n return $entity->getUser()->getId() === $this->getUser()->getId();\n})\n
Usage in a CRUD template: {% if configuration.action('index', 'new')%}...{% endif %}.
"},{"location":"crud/configuration/#setactiontitle","title":"setActionTitle","text":"setActionTitle(string $page, string $action, string $title)
Set the title of an action in the given page.
Example of usage in a CRUD template: {{ configuration.actionTitle(context, 'new', 'New')|trans }}
"},{"location":"crud/configuration/#setview","title":"setView","text":"setView(string $context, string $view)
Override a view.
Controller (context) View Description index @Core/admin/crud/index.html.twig Template of the page index edit @Core/admin/crud/edit.html.twig Template of the page edit new @Core/admin/crud/new.html.twig Template of the page new show @Core/admin/crud/show.html.twig Template of the page show filter @Core/admin/crud/filter.html.twig Template of the page filter Form (context) View Description form @Core/admin/crud/_form.html.twig Template to render a form form_widget @Core/admin/crud/_form_widget.html.twig Template to render a form widget form_translations @Core/admin/crud/_form_translations.html.twig Template to render a the translation field Entity (context) View Description show_entity @Core/admin/crud/_show.html.twig Template to render the entity"},{"location":"crud/configuration/#setshowactions","title":"setShowActions","text":"setShowActions(string $context, bool $value)
Show or hide the column Actions.
"},{"location":"crud/configuration/#setviewdatas","title":"setViewDatas","text":"setViewDatas(string $context,array$datas) and addViewData(string $context, string $name, $value)
Add datas given to a view. Useful in a custom controller.
"},{"location":"crud/configuration/#setfield","title":"setField","text":"setField(string $context, string $label, string $field,array$options)
Add a field displayed in the given context. Used in the index.
use App\\Core\\Crud\\Field;\n\n$configuration->setField('index', 'Title', Field\\TextField::class, [\n // options\n])\n
All fields have these options:
Option Type Default Description property string null Entity's property to display property__builder callable null A callable data and used to generate the content displayed view string @Core/admin/crud/field/text.html.twig The templated rendered default_value string null Default value to display when the property is null action string null An action to perform on click (null, edit, view) raw boolean false Render as HTML sort array | callable null Defines how to sort href string | callable null Data to generate a link href_attr array | callable null Attributes of the link inline_form null | callable null A method to define a form to edit datas inline_form_validation null | callable null A method to define a custom form validation callback Example #0$configuration->setField('index', 'My field', TextField::class, [\n 'property' => 'myProperty',\n // OR\n 'property_builder' => function($entity, array $options) {\n return $entity->getMyProperty();\n },\n])\n
Example #1$configuration->setField('index', 'My field', TextField::class, [\n 'raw' => true,\n 'property_builder' => function($entity, array $options) {\n return sprintf('<span class=\"foo\">%s</span>', $entity->getBar());\n },\n])\n
Example #2// https://127.0.0.7:8000/admin/my_entity?_sort=property&_sort_direction=asc\n$configuration->setField('index', 'My field', TextField::class, [\n 'property' => 'myProperty'\n 'sort' => ['property', '.myProperty'],\n // OR\n 'sort' => ['property', function(MyEntityRepositoryQuery $query, $direction) {\n $query->orderBy('.myProperty', $direction);\n }],\n])\n
Example #3use Symfony\\Component\\Form\\FormBuilderInterface;\nuse Symfony\\Component\\Form\\Extension\\Core\\Type\\TextType;\nuse Symfony\\Component\\Validator\\Constraints\\NotBlank;\n\n$configuration->setField('index', 'My field', TextField::class, [\n 'property' => 'myProperty',\n 'inline_form' => function(FormBuilderInterface $builder, EntityInterface $entity) {\n $builder->add('myProperty', TextType::class, [\n 'required' => true,\n 'constraints' => [new NotBlank()],\n ]);\n }\n])\n
"},{"location":"crud/configuration/#textfield","title":"TextField","text":"App\\Core\\Crud\\Field\\TextField
Option Type Default Description view string @Core/admin/crud/field/text.html.twig The templated rendered"},{"location":"crud/configuration/#datefield","title":"DateField","text":"App\\Core\\Crud\\Field\\DateField
Option Type Default Description view string @Core/admin/crud/field/date.html.twig The templated rendered format string Y-m-d The date format"},{"location":"crud/configuration/#datetimefield","title":"DatetimeField","text":"App\\Core\\Crud\\Field\\DatetimeField
Option Type Default Description view string @Core/admin/crud/field/date.html.twig The templated rendered format string Y-m-d H:i:s The date format"},{"location":"crud/configuration/#buttonfield","title":"ButtonField","text":"App\\Core\\Crud\\Field\\ButtonField
Option Type Default Description view string @Core/admin/crud/field/button.html.twig The templated rendered button_attr array [] Button HTML attributes button_attr_builder callabled null A callable data and used to generate button__attr button_tag string button HTML tag of the button"},{"location":"crud/configuration/#imagefield","title":"ImageField","text":"App\\Core\\Crud\\Field\\ImageField
Option Type Default Description view string @Core/admin/crud/field/image.html.twig The templated rendered image_attr array [] Image HTML attributes"},{"location":"crud/configuration/#booleanfield","title":"BooleanField","text":"App\\Core\\Crud\\Field\\BooleanField
Option Type Default Description view string @Core/admin/crud/field/boolean.html.twig The templated rendered display string toggle Type of render (toggle or checkbox) checkbox_class_when_true string fa-check-square HTML class added when the value is true and display is checkbox checkbox_class_when_false string fa-square HTML class added when the value is false and display is checkbox toggle_class_when_true string bg-success HTML class added when the value is true and display is toggle toggle_class_when_false string bg-secondary HTML class added when the value is false and display is toggle"},{"location":"crud/configuration/#setmaxperpage","title":"setMaxPerPage","text":"setMaxPerPage(string $page, int $max)
Set how many elements are displayed in a single page.
"},{"location":"crud/configuration/#seti18n","title":"setI18n","text":"setI18n(array $locales, string $defaultLocale)
Set an array of locales for a translatable entity. The default locale is used in the index page. Compatible with https://github.com/KnpLabs/DoctrineBehaviors/blob/master/docs/translatable.md.
"},{"location":"crud/configuration/#setdefaultsort","title":"setDefaultSort","text":"setDefaultSort(string $context, string $label, string $direction = 'asc')
Set the default sort applied in the repository query.
$configuration\n ->setDefaultSort('index', 'title', 'asc')\n ->setField('index', 'Title', Field\\TextField::class, [\n 'property' => 'title',\n 'sort' => ['title', '.title'],\n ]);\n
"},{"location":"crud/configuration/#setissortablecollection","title":"setIsSortableCollection","text":"setIsSortableCollection(string $page, bool $isSortableCollection)
It enables the drag & drop to sort entities.
class MyEntity implements EntityInterface\n{\n // ...\n\n /**\n * @ORM\\Column(type=\"integer\", nullable=true)\n */\n private $sortOrder;\n\n public function getSortOrder(): ?int\n {\n return $this->sortOrder;\n }\n\n public function setSortOrder(?int $sortOrder): self\n {\n $this->sortOrder = $sortOrder;\n\n return $this;\n }\n\n // ...\n}\n
"},{"location":"crud/configuration/#setsortablecollectionproperty","title":"setSortableCollectionProperty","text":"setSortableCollectionProperty(string $sortableCollectionProperty)
In order to sort entities, the default property used is sortOrder. You can set something else.
"},{"location":"crud/configuration/#setlistrowattributes","title":"setListRowAttributes","text":"setListRowAttributes(string $context, array $attributes)
Add attributes on a row (list).
$configuration->setListRowAttributes('index', [\n 'class' => 'foo',\n 'data-foo' => function(Entity $entity) {\n return $entity->getFoo(); // A string\n },\n]);\n
"},{"location":"crud/configuration/#setbatchaction","title":"setBatchAction","text":"setBatchAction(string $context, string $action, string $label, callable $callack)
Add a batch action. The callback has 2 arguments:
- An instance of
App\\Core\\Entity\\EntityInterface - An instance of
App\\Core\\Manager\\EntityManager
use App\\Core\\Entity\\EntityInterface;\nuse App\\Core\\Manager\\EntityManager;\n\n$configuration->setBatchAction(\n 'index',\n 'delete',\n 'Delete',\n function(EntityInterface $entity, EntityManager $manager) {\n $manager->delete($entity);\n }\n);\n
"},{"location":"crud/configuration/#setglobalbatchaction","title":"setGlobalBatchAction","text":"setGlobalBatchAction(string $context, string $action, string $label, callable $callack)
Add a global batch action. The callback has 3 arguments:
- An instance of
App\\Core\\Repository\\RepositoryQuery - An instance of
App\\Core\\Manager\\EntityManager - An array of selected entities or a
null value
Do not use the same action in global and classic batch action.
The callback can return a response. If not, the user will be redirect automatically. See the example below:
use App\\Core\\Entity\\RepositoryQuery;\nuse App\\Core\\Manager\\EntityManager;\nuse Symfony\\Component\\HttpFoundation\\JsonResponse;\n\n$configuration->setGlobalBatchAction(\n 'index',\n 'export_json',\n 'Export to JSON',\n function(RepositoryQuery $query, EntityManager $manager, ?array $selection): JsonResponse {\n $items = $selection ?? $query->find();\n\n return $this->json($items);\n }\n);\n
"},{"location":"crud/generator/","title":"Generator","text":""},{"location":"crud/generator/#prepare-your-entity","title":"Prepare your entity","text":" - implements
App\\Core\\Entity\\EntityInterface (see Entity Manager) - creates a repository query (see Repository Query)
- creates a factory (see Factory)
- generates a form (see
php bin/console make:form --help)
"},{"location":"crud/generator/#generation","title":"Generation","text":"The generation is performed in CLI. These information are required:
- The name of the futur controller (eg:
MyEntityAdminController) - The namespace of the entity (eg:
MyEntity) - The namespace of the entity repository query (eg:
MyEntityRepositoryQuery) - The namespace of the the entity factory (eg:
MyEntityFactory) - The namespace of the form used to create and update the entity (eg:
MyEntityType)
Simply run php bin/console make:crud-controller.
"},{"location":"entities/em/","title":"Entity Manager","text":"The entity manager of Muprh is a proxy of the Doctrine's entity manager. It gives you an easy way to create, update and delete an entity and dispatches events easy to subscribe to.
"},{"location":"entities/em/#implementation","title":"Implementation","text":"Entities must implements App\\Core\\Entity\\EntityInterface.
src/Entity/MyEntity.phpnamespace App\\Entity;\n\nuse App\\Repository\\MyEntityRepository;\nuse Doctrine\\ORM\\Mapping as ORM;\nuse App\\Core\\Entity\\EntityInterface;\n\n#[ORM\\Entity(repositoryClass: MyEntityRepository::class)]\nclass MyEntity implements EntityInterface\n{\n // ...\n}\n
"},{"location":"entities/em/#usage","title":"Usage","text":"There are 2 entity managers which are services:
App\\Core\\Manager\\EntityManager used for all entities App\\Core\\Manager\\TranslatableEntityManager used for translatable entities
src/Controller/FooController.phpnamespace App\\Controller;\n\nuse App\\Core\\Manager\\EntityManager\nuse App\\Entity\\MyEntity;\nuse Symfony\\Component\\HttpFoundation\\Response;\n\nclass FooController\n{\n public function foo(EntityManager $entityManager): Response\n {\n $myEntity = new MyEntity();\n\n // Creates an entity\n $entityManager->create($myEntity);\n\n // Updates an entity\n $entityManager->update($myEntity);\n\n // Deletes an entity\n $entityManager->delete($myEntity);\n\n // ...\n }\n}\n
"},{"location":"entities/em/#events","title":"Events","text":"Events are dispatched before and after creation, update and deletion. All entities of Muprh use the entity manager.
src/EventSubscriber/MyEntityEventSubscriber.phpnamespace App\\EventSubscriber;\n\nuse App\\Core\\Entity\\EntityInterface;\nuse App\\Core\\Event\\EntityManager\\EntityManagerEvent;\nuse App\\Core\\EventSubscriber\\EntityManagerEventSubscriber;\nuse App\\Entity\\MyEntity;\n\nclass MyEntityEventSubscriber extends EntityManagerEventSubscriber\n{\n public function supports(EntityInterface $entity): bool\n {\n return $entity instanceof MyEntity;\n }\n\n public function onCreate(EntityManagerEvent $event)\n {\n if (!$this->supports($event->getEntity())) {\n return;\n }\n\n // ...\n }\n\n public function onUpdate(EntityManagerEvent $event)\n {\n if (!$this->supports($event->getEntity())) {\n return;\n }\n\n // ...\n }\n\n public function onDelete(EntityManagerEvent $event)\n {\n if (!$this->supports($event->getEntity())) {\n return;\n }\n\n // ...\n }\n\n public function onPreCreate(EntityManagerEvent $event)\n {\n if (!$this->supports($event->getEntity())) {\n return;\n }\n\n // ...\n }\n\n public function onPreUpdate(EntityManagerEvent $event)\n {\n if (!$this->supports($event->getEntity())) {\n return;\n }\n\n // ...\n }\n\n public function onPreDelete(EntityManagerEvent $event)\n {\n if (!$this->supports($event->getEntity())) {\n return;\n }\n\n // ...\n }\n}\n
"},{"location":"entities/factory/","title":"Factory","text":"Each entity should have a factory that helps to generate a new entity. A factory must implements App\\Core\\Factory\\FactoryInterface.
"},{"location":"entities/factory/#generation","title":"Generation","text":"A factory is basically a service which contain at lease a method named create.
The generation is performed in CLI. These information are required:
- The name of the futur factory (eg:
MyEntityFactory) - The namespace of the entity (eg:
MyEntity)
Simply run php bin/console make:factory.
"},{"location":"entities/factory/#usage","title":"Usage","text":"src/Controller/FooController.phpnamespace App\\Controller;\n\nuse App\\Factory\\MyEntityFactory;\nuse Symfony\\Component\\HttpFoundation\\Response;\n\nclass FooController\n{\n public function foo(MyEntityFactory $factory): Response\n {\n $entity = $factory->create();\n\n // ...\n }\n}\n
"},{"location":"entities/query/","title":"Repository Query","text":"A Repository query is an abstraction of the doctrine repository.
"},{"location":"entities/query/#requirement","title":"Requirement","text":"Entities must implements App\\Core\\Entity\\EntityInterface.
src/Entity/MyEntity.phpnamespace App\\Entity;\n\nuse App\\Repository\\MyEntityRepository;\nuse Doctrine\\ORM\\Mapping as ORM;\nuse App\\Core\\Entity\\EntityInterface;\n\n#[ORM\\Entity(repositoryClass: MyEntityRepository::class)]\nclass MyEntity implements EntityInterface\n{\n // ...\n}\n
"},{"location":"entities/query/#generation","title":"Generation","text":"The generation is performed in CLI. These information are required:
- The namespace of the repository (eg:
MyEntityRepository)
Simply run php bin/console make:repository-query.
Each entity has its own repository query which is a service.
src/Repository/MyEntityRepositoryQuerynamespace App\\Repository;\n\nuse App\\Core\\Repository\\RepositoryQuery;\nuse Knp\\Component\\Pager\\PaginatorInterface;\n\nclass MyEntityRepositoryQuery extends RepositoryQuery\n{\n public function __construct(MyEntityRepository $repository, PaginatorInterface $paginator)\n {\n parent::__construct($repository, 'm', $paginator);\n }\n\n // Example of custom filter\n public function filterByFooBar(bool $foo, bool $bar): self\n {\n $this\n ->andWhere('.foo = :foo')\n ->andWhere('.bar = :bar')\n ->setParameter(':foo', $foo)\n ->setParameter(':bar', $bar);\n\n return $this;\n }\n}\n
"},{"location":"entities/query/#usage","title":"Usage","text":"You are able to find entities in an easy way, without knowing the identification variable and without creating a query builder.
src/Controller/FooController.phpnamespace App\\Controller;\n\nuse App\\Repository\\MyEntityRepositoryQuery\nuse Symfony\\Component\\HttpFoundation\\Response;\n\nclass FooController\n{\n public function foo(MyEntityRepositoryQuery $query): Response\n {\n $entities = $query->create()->find();\n $entity = $query->create()->findOne();\n\n // Filtered and sorted entities\n $entities = $query->create()\n ->orderBy('.id', 'DESC')\n ->where('.isPublished = true')\n ->find();\n\n // ...\n }\n}\n
"},{"location":"entities/query/#custom-methods","title":"Custom methods","text":"// ...\n\nclass MyEntityRepositoryQuery extends RepositoryQuery\n{\n // ...\n\n public function filterByFooBar(bool $foo, bool $bar): self\n {\n $this\n ->andWhere('.foo = :foo')\n ->andWhere('.bar = :bar')\n ->setParameter(':foo', $foo)\n ->setParameter(':bar', $bar);\n\n return $this;\n }\n}\n
$entities = $query->create()\n ->filterByFoo($foo, $bar)\n ->find();\n
In the context of a CRUD, filters are applied using the method useFilters. Integers, strings and booleans are automatically processed. Other types are passed to the method filterHandler.
You have to override it to manage them, example:
// ...\n\nuse App\\Entity\\Something;\n\nclass MyEntityRepositoryQuery extends RepositoryQuery\n{\n // ...\n\n public function filterHandler(string $name, $value): self\n {\n if ($name === 'something' && $value instanceof Something) {\n $this\n ->join('something', 's')\n ->andWhere('s.id = :something')\n ->setParameter('something', $value->getId())\n ;\n }\n }\n}\n
You can also force filterHandler te be used for specific filter field:
// ...\n\nclass MyEntityRepositoryQuery extends RepositoryQuery\n{\n public function __construct(Repository $repository, PaginatorInterface $paginator)\n {\n // ...\n\n $this->addForcedFilterHandler('foo');\n }\n\n public function filterHandler(string $name, $value): self\n {\n // ...\n\n if ($name === 'foo) {\n // ...\n }\n }\n\n}\n
"},{"location":"entities/query/#pager","title":"Pager","text":"You can paginate entities (Knp\\Component\\Pager\\Pagination\\PaginationInterface):
$pager = $query->create()->paginate($page, $maxPerPage);\n
"},{"location":"settings/global/","title":"Global settings","text":""},{"location":"settings/global/#create-settings","title":"Create settings","text":"The creation of settings is based on events.
Using an event subscriber, you can create settings and define how to edit them. A setting's value is stored in json so a value could be a string, a boolean, an array, etc.
See the example below.
src/EventSubscriber/SettingEventSubscriber.phpnamespace App\\EventSubscriber;\n\nuse App\\Core\\Event\\Setting\\SettingEvent;\nuse App\\Core\\EventSubscriber\\SettingEventSubscriber as EventSubscriber;\nuse App\\Core\\Setting\\SettingManager;\nuse Symfony\\Component\\Form\\Extension\\Core\\Type\\CheckboxType;\nuse Symfony\\Component\\Form\\Extension\\Core\\Type\\ColorType;\n\nclass SettingEventSubscriber extends EventSubscriber\n{\n protected SettingManager $manager;\n\n public function __construct(SettingManager $manager)\n {\n $this->manager = $manager;\n }\n\n public function onInit(SettingEvent $event)\n {\n $this->manager->init('app_font_color', 'Design', 'Font color', , '#fff');\n $this->manager->init('app_background_color', 'Design', 'Background color', '#333');\n $this->manager->init('app_maintenance_enabled', 'System', 'Maintenance', false);\n }\n\n public function onFormInit(SettingEvent $event)\n {\n $data = $event->getData();\n $builder = $data['builder'];\n $entity = $data['entity'];\n\n if (in_array($entity->getCode(), ['app_font_color', 'app_background_color'])) {\n $builder->add(\n 'value',\n ColorType::class,\n [\n 'label' => $entity->getLabel(),\n ]\n );\n }\n\n if (in_array($entity->getCode(), ['app_maintenance_enabled'])) {\n $builder->add(\n 'value',\n CheckboxType::class,\n [\n 'label' => $entity->getLabel(),\n ]\n );\n }\n }\n}\n
Result:
"},{"location":"settings/global/#access-settings","title":"Access settings","text":"Settings are accessible using App\\Core\\Setting\\SettingManager which is a service.
src/Controller/FooController.phpnamespace App\\Controller;\n\nuse App\\Core\\Setting\\SettingManager;\nuse Symfony\\Component\\HttpFoundation\\Response;\n\nclass FooController\n{\n public function foo(SettingManager $settingManager): Response\n {\n $fontColor = $settingManager->get('app_font_color');\n $backgroundColor = $settingManager->get('app_background_color');\n $maintenanceEnabled = $settingManager->get('app_maintenance_enabled');\n\n // ...\n }\n}\n
In a template, you can use the function setting:
Font color: {{ setting('app_font_color') }}<br>\nBackground color: {{ setting('app_background_color') }}<br>\nMaintenance enabled: {{ setting('app_maintenance_enabled') ? 'Yes' : 'No' }}<br>\n
"},{"location":"settings/global/#update-settings","title":"Update settings","text":"Settings are accessible using App\\Core\\Setting\\SettingManager which is a service.
src/Controller/FooController.phpnamespace App\\Controller;\n\nuse App\\Core\\Setting\\SettingManager;\nuse Symfony\\Component\\HttpFoundation\\Response;\n\nclass FooController\n{\n public function foo(SettingManager $settingManager): Response\n {\n $settingManager->set('app_font_color', '#f00');\n $settingManager->set('app_background_color', '#00f');\n $settingManager->set('app_maintenance_enabled', true);\n\n // ...\n }\n}\n
You can also edit them from UI:
"},{"location":"settings/global/#options","title":"Options","text":"You can add options using this way:
$event->setOption('view', 'large');\n
Available options:
view (default: false): show a large modal
"},{"location":"settings/navigation/","title":"Navigation settings","text":""},{"location":"settings/navigation/#create-settings","title":"Create settings","text":"The creation of settings is based on events.
Using an event subscriber, you can create settings and define how to edit them. A setting's value is stored in json so a value could be a string, a boolean, an array, etc.
See the example below.
src/EventSubscriber/NavigationSettingEventSubscriber.php
namespace App\\EventSubscriber;\n\nuse App\\Core\\Event\\Setting\\NavigationSettingEvent;\nuse App\\Core\\EventSubscriber\\NavigationSettingEventSubscriber as EventSubscriber;\nuse App\\Core\\Setting\\NavigationSettingManager;\nuse Symfony\\Component\\Form\\Extension\\Core\\Type\\TextType;\nuse Symfony\\Component\\Form\\Extension\\Core\\Type\\EmailType;\n\nclass NavigationSettingEventSubscriber extends EventSubscriber\n{\n protected NavigationSettingManager $manager;\n\n public function __construct(NavigationSettingManager $manager)\n {\n $this->manager = $manager;\n }\n\n public function onInit(NavigationSettingEvent $event)\n {\n $data = $event->getData();\n $navigation = $data['navigation'];\n\n $this->manager->init($navigation, 'nav_tracker_code', 'Stats', 'Tracker', '');\n $this->manager->init($navigation, 'nav_contact_email', 'Contact', 'Email', 'foo@example.com');\n }\n\n public function onFormInit(NavigationSettingEvent $event)\n {\n $data = $event->getData();\n $builder = $data['builder'];\n $entity = $data['entity'];\n\n if (in_array($entity->getCode(), ['nav_tracker_code'])) {\n $builder->add(\n 'value',\n TextType::class,\n [\n 'label' => $entity->getLabel(),\n ]\n );\n }\n\n if (in_array($entity->getCode(), ['nav_contact_email'])) {\n $builder->add(\n 'value',\n EmailType::class,\n [\n 'label' => $entity->getLabel(),\n ]\n );\n }\n }\n}\n
Result: "},{"location":"settings/navigation/#access-settings","title":"Access settings","text":"Settings are accessible using App\\Core\\Setting\\NavigationSettingManager which is a service.
src/Controller/FooController.phpnamespace App\\Controller;\n\nuse App\\Core\\Setting\\NavigationSettingManager;\nuse App\\Core\\Site\\SiteRequest;\nuse Symfony\\Component\\HttpFoundation\\Response;\n\nclass FooController\n{\n public function foo(NavigationSettingManager $settingManager, SiteRequest $siteRequest): Response\n {\n $trackerCode = $settingManager->get($siteRequest->getNavigation(), 'nav_tracker_code');\n $contactEmail = $settingManager->get('my_nav', 'nav_contact_email');\n\n // ...\n }\n}\n
In a template, you can use the function navigation_setting:
Tracker code: {{ navigation_setting(_navigation, 'nav_tracker_code') }}<br>\nContact email: {{ navigation_setting('my_nav', 'nav_contact_email') }}<br>\n
"},{"location":"settings/navigation/#update-settings","title":"Update settings","text":"Settings are accessible using App\\Core\\Setting\\NavigationSettingManager which is a service.
src/Controller/FooController.phpnamespace App\\Controller;\n\nuse App\\Core\\Setting\\NavigationSettingManager;\nuse App\\Core\\Site\\SiteRequest;\nuse Symfony\\Component\\HttpFoundation\\Response;\n\nclass FooController\n{\n public function foo(NavigationSettingManager $settingManager, SiteRequest $siteRequest): Response\n {\n $settingManager->set($siteRequest->getNavigation(), 'nav_tracker_code', '...');\n $settingManager->set('my_nav', 'nav_contact_email', '...');\n\n // ...\n }\n}\n
You can also edit them from UI:
"},{"location":"tree/","title":"Tree manager","text":"Murph manages contents this way:
- You define navigations
- For each navigation, you define menus
- For each menu, you define nodes (elements)
- and for each node, you define:
- an optional page
- the routing
- some attributes
- a sitemap configuration
"},{"location":"tree/#diagram","title":"Diagram","text":"%%{\n init: {\n \"theme\": \"dark\",\n \"flowchart\": {\n \"curve\": \"cardinal\"\n }\n }\n}%%\n\ngraph TB\n N1[Navigation 1] --> M1[Menu 1];\n N2[Navigation ...];\n NX[Navigation N];\n\n N1 --> M2[Menu ...];\n N1 --> MX[Menu N];\n\n N2 --> L1[...]\n NX --> L2[...]\n\n M1 --> MN1[Node 1]\n M1 --> MN2[Node ...]\n M1 --> MN3[Node N]\n\n M2 --> L3[...]\n MX --> L4[...]\n\n MN1 --> P1[Page]\n\n P1 --> B1[Block 1]\n P1 --> B2[Block ...]\n P1 --> BN[Block N]
"},{"location":"tree/menu/","title":"Menu","text":"To create a menu, go to Trees, select the navigation and click on Add a menu. Then fill the form and save.
- The
label is the label displayed whenever necessary (eg: Top menu) - The
code is an unique technical identifier (in the given navigation) and it is useful in templating, routing and settings (eg: top)
When a menu is created then an node is automatically generated.
"},{"location":"tree/navigation/","title":"Navigation","text":"To create a navigation, go to Navigations and click on New. Then fill the form and save.
- The
label is the label displayed whenever necessary (eg: Example) - The
locale is the language used in the content (eg: en) - The
code is a unique technical identifier useful in templating, routing and settings (eg: example_en) - The
domain defines the main domain used to access the navigation (eg: example.com) Additional domains are additional domains used to access the navigation (eg: www.example.com). You can specify regular expression to match all that you want
If several navigations share the same domain, then the locale will by used to prefix routes. But if a navigation uses a single domain then the local will not prefix routes.
"},{"location":"tree/node/","title":"Node","text":"A node allows you to create a tree structure in a menu. To create or update a node, click on \"Edit\" or the sign \"+\". The basic information to fill is label but more parameters are accessible via 4 tabs:
- Content
- Routing
- Attributes
- Sitemap
"},{"location":"tree/node/#content","title":"Content","text":"The content tab allow you to define an optional Page to associate to the node. You can also define that the node is an alias of another node.
"},{"location":"tree/node/#routing","title":"Routing","text":"The routing tab is very important. It allows you to define all parameters related to:
- The routing (obviously):
- An optional URL with parameters
- A route name generated using the field
code
- A custom content-type of the response (eg: 'text/plain')
- A custom controller called when the node is requested
To add a controller in the list, edit config/packages/app.yaml:
core:\n site:\n controllers:\n - {name: 'Foo', action: 'App\\Controller\\ExampleController::foo'}\n - {name: 'Bar', action: 'App\\Controller\\OtherController::bar'}\n
If you need to restrict the access, you can provided a list of roles in the configuration:
config/packages/app.yamlcore:\n site:\n security:\n roles:\n - {name: 'Foo role', role: 'ROLE_FOO'}\n - {name: 'Bar role', role: 'ROLE_BAR'}\n
Then you will be able to select what roles are required:
"},{"location":"tree/node/#attributes","title":"Attributes","text":"Attributes are a collection of keys and values attached to a node (eg: class, icon, whatever you want).
"},{"location":"tree/node/#sitemap","title":"Sitemap","text":"This tab contains information to configure the sitemap.
"},{"location":"tree/page/","title":"Page","text":"A page is a doctrine entity that contains blocks and form builder. You can run php bin/console make:page and generate a new page in an interactive way.
src/Entity/Page/YourPage.phpnamespace App\\Entity\\Page;\n\nuse App\\Core\\Entity\\Site\\Page\\Block;\nuse App\\Core\\Entity\\Site\\Page\\Page;\nuse App\\Core\\Form\\Site\\Page\\TextBlockType;\nuse Doctrine\\ORM\\Mapping as ORM;\nuse Symfony\\Component\\Form\\FormBuilderInterface;\n\n#[ORM\\Entity]\nclass YourPage extends Page\n{\n public function buildForm(FormBuilderInterface $builder, array $options)\n {\n $builder->add(\n 'myBlock',\n TextBlockType::class,\n [\n 'label' => 'My block',\n 'options' => [\n // options given to the sub form\n ],\n ]\n );\n\n // ...\n }\n\n public function setMyBlock(Block $block)\n {\n return $this->setBlock($block);\n }\n\n public function getMyBlock(): Block\n {\n return $this->getBlock('myBlock');\n }\n\n // ...\n}\n
Then edit config/packages/app.yaml and add your page:
core:\n site:\n pages:\n App\\Entity\\Page\\SimplePage:\n name: 'Simple page'\n templates:\n - {name: \"Default\", file: \"page/simple/default.html.twig\"}\n App\\Entity\\Page\\YourPage:\n name: 'Your page'\n templates:\n - {name: \"Template 1\", file: \"page/your_page/template1.html.twig\"}\n - {name: \"Template 2\", file: \"page/your_page/template2.html.twig\"}\n
"},{"location":"tree/page/#blocks","title":"Blocks","text":""},{"location":"tree/page/#textblocktype","title":"TextBlockType","text":"App\\Core\\Form\\Site\\Page\\TextBlockType will render a symfony TextType.
"},{"location":"tree/page/#textareablocktype","title":"TextareaBlockType","text":"App\\Core\\Form\\Site\\Page\\TextareaBlockType will render a symfony TextareaType.
"},{"location":"tree/page/#choiceblocktype","title":"ChoiceBlockType","text":"App\\Core\\Form\\Site\\Page\\ChoiceBlockType will render a symfony ChoiceType.
"},{"location":"tree/page/#fileblocktype","title":"FileBlockType","text":"App\\Core\\Form\\Site\\Page\\FileBlockType will render a symfony FileType with a download link.
In the getter, you must specify the block:
use App\\Core\\Entity\\Site\\Page\\FileBlock;\n\npublic function getMyBlock(): Block\n{\n return $this->getBlock('myBlock', FileBlock::class);\n}\n
"},{"location":"tree/page/#filepickerblocktype","title":"FilePickerBlockType","text":"App\\Core\\Form\\Site\\Page\\FilePickerBlockType will render a specific widget that use the file manager.
"},{"location":"tree/page/#editorjstextareablocktype","title":"EditorJsTextareaBlockType","text":"App\\Core\\Form\\Site\\Page\\EditorJsTextareaBlockType will render a EditorJs widget.
"},{"location":"tree/page/#grapesjsblocktype","title":"GrapesJsBlockType","text":"App\\Core\\Form\\Site\\Page\\GrapesJsBlockType will render a GrapesJS editor.
"},{"location":"tree/page/#tinymcetextareablocktype","title":"TinymceTextareaBlockType","text":"App\\Core\\Form\\Site\\Page\\TinymceTextareaBlockType will render a Tinymce editor.
"},{"location":"tree/page/#imageblocktype","title":"ImageBlockType","text":"App\\Core\\Form\\Site\\Page\\ImageBlockType will render a symfony FileType with a preview of the image.
In the getter, you must specify the block:
use App\\Core\\Entity\\Site\\Page\\FileBlock;\n\npublic function getMyBlock(): Block\n{\n return $this->getBlock('myBlock', FileBlock::class);\n}\n
"},{"location":"tree/page/#collectionblocktype","title":"CollectionBlockType","text":"App\\Core\\Form\\Site\\Page\\CollectionBlockType will a render a symfony CollectionType with availabity to add and remove elements.
use App\\Core\\Entity\\Site\\Page\\CollectionBlock;\n\npublic function getMyBlock(): Block\n{\n return $this->getBlock('myBlock', CollectionBlock::class);\n}\n
"},{"location":"tree/page/#event","title":"Event","text":"When a page is being edited, the options can be set as follows:
src/EventSubscriber/PageEventSubscriber.phpnamespace App\\EventSubscriber;\n\nuse App\\Core\\Event\\Page\\PageEditEvent;\nuse App\\Entity\\Page\\YourPage;\n\nclass PageEventSubscriber implements EventSubscriberInterface\n{\n public static function getSubscribedEvents()\n {\n return [\n PageEditEvent::FORM_INIT_EVENT => ['onFormInit'],\n ];\n }\n\n public function onFormInit(PageEditEvent $event)\n {\n if ($event->getPage() instanceof YourPage) {\n $event->addPageBuilderOptions([\n // options\n ]);\n }\n }\n}\n
"},{"location":"utils/cache/","title":"Cache Manager","text":" - Service:
App\\Core\\Cache\\SymfonyCacheManager - Methods:
cleanRouting(): clear the cache of routes cleanAll(OutputInterface $output = null): clean and warmup all cache
"},{"location":"utils/doctrine/","title":"Doctrine","text":""},{"location":"utils/doctrine/#timestampable","title":"Timestampable","text":"App\\Core\\Doctrine\\Timestampable is a trait usuble in an entity. It adds createdAt and updatedAt datetime attributes with the setters and the getters :
setCreatedAt(?\\DateTime $createdAt): self setUpdated(?\\DateTime $createdAt): self getCreatedAt(): ?\\DateTime getUpdatedAt(): ?\\DateTime
When the entity is created or updated, createdAt and updatedAt are automatically updated to.
"},{"location":"utils/doctrine/#usage","title":"Usage","text":"src/Entity/FooEntity.phpnamespace App/Entity;\n\nuse use App\\Core\\Entity\\EntityInterface;\nuse Doctrine\\ORM\\Mapping as ORM;\n\nuse App\\Core\\Doctrine\\Timestampable;\nuse App\\Core\\Entity\\EntityInterface;\nuse App\\Repository\\FooRepository;\nuse Doctrine\\ORM\\Mapping as ORM;\n\n#[ORM\\Entity(repositoryClass: FooRepository::class)]\n#[ORM\\HasLifecycleCallbacks]\nclass FooEntity implements EntityInterface\n{\n use Timestampable;\n\n // ...\n}\n
"},{"location":"utils/file_attribute/","title":"File attribute","text":"App\\Core\\File\\FileAttribute::handleFile transforms a file path to an instance of Symfony\\Component\\HttpFoundation\\File\\File. You can specify another class if needed. If the path is null or if the file does not exist, it returns null.
use App\\Core\\File\\FileAttribute;\nuse App\\Foo\\Bar;\n\n$path = 'path/to/file';\n$file = FileAttribute::handleFile($path); // returns an instance of File\n\n$path = 'path/to/file';\n$file = FileAttribute::handleFile($path, Bar::class); // returns an instance of Bar\n\n$path = 'path/to/removed_file';\n$file = FileAttribute::handleFile($path); // returns null\n\n$path = null;\n$file = FileAttribute::handleFile($path); // returns null\n
"},{"location":"utils/file_handler/","title":"File upload handler","text":"App\\Core\\Form\\FileUploadHandler is a service and helps you to upload a file. See example below.
use App\\Core\\Form\\FileUploadHandler;\nuse App\\Entity\\Foo;\nuse App\\Form\\FooType;\nuse Symfony\\Component\\HttpFoundation\\Request;\n\npublic function upload(Request $request, FileUploadHandler $fileUpload)\n{\n $entity = new Foo();\n $form = $this->createForm(FooType::class, $foo);\n\n if ($request->isMethod('POST')) {\n $form->handleRequest($request);\n\n if ($form->isValid()) {\n $fileDirectory = 'uploads/';\n $keepOriginalFilename = false;\n\n // single file\n $fileUpload->handleForm(\n uploadedFile: $form->get('image')->getData(), // Symfony\\Component\\HttpFoundation\\File\\UploadedFile or null\n path: $fileDirectory,\n // optional\n afterUploadCallback: function ($filename) use ($entity, $fileDirectory) {\n $entity->setImage($fileDirectory.$filename);\n },\n // optional\n keepOriginalFilename: $keepOriginalFilename\n );\n\n // multiple files\n $fileUpload->handleForm(\n uploadedFile: $form->get('images')->getData(), // Symfony\\Component\\HttpFoundation\\File\\UploadedFile or null\n path: $fileDirectory,\n // optional\n afterUploadCallback: function ($filename) use ($entity, $fileDirectory) {\n $entity->addImage($fileDirectory.$filename);\n },\n // optional\n afterUploadsCallback: function (array $filenames) use ($entity, $fileDirectory) {\n foreach ($filenames as $filename) {\n $entity->addImage($fileDirectory.$filename);\n }\n },\n // optional\n keepOriginalFilename: $keepOriginalFilename\n );\n\n\n // ...\n }\n }\n}\n
If you need to generate custom filenames, FileUploadHandler allows you to define a generator:
use Symfony\\Component\\HttpFoundation\\File\\UploadedFile;\n\n$fileUpload->setFilenameGenerator(function(UploadedFile $file) {\n return sprintf('%d.%s', mt_rand(), $file->guessExtension());\n});\n
"},{"location":"utils/mail/","title":"Mail","text":"App\\Core\\Notification\\MailNotifier is a service and helps you to create a mail using twig template.
Useful methods:
setRecipients(array $recipients): MailNotifier setBccRecipients(array $bccRecipients): MailNotifier setSubject(?string $subject): MailNotifier setFrom($from): MailNotifier setReplyTo($replyTo): MailNotifier setAttachments(array $attachments): MailNotifier addRecipient(string $email, bool $isBcc = false): MailNotifier addRecipients(array $emails, bool $isBcc = false): MailNotifier addRecipientByUser(\\App\\Entity\\User $user, bool $isBcc = false): self addRecipientsByUsers($users, bool $isBcc = false): self addAttachment(string $attachment): MailNotifier addAttachments(array $attachments): MailNotifier init(): MailNotifier notify(string $template, array $data = [], string $type = 'text/html'): MailNotifier
Exemple:
use App\\Core\\Notification\\MailNotifier;\nuse App\\Repository\\UserRepositoryQuery;\n\npublic function foo(MailNotifier $notifier, UserRepositoryQuery $query)\n{\n // ...\n\n $notifier\n ->init()\n ->setSubject('Your bill')\n ->addRecipient('john.doe@example.com')\n ->addRecipientsByUsers($query->create()->where('.isAdmin = true')->find(), true)\n ->addAttachment('path/to/bill.pdf')\n ->notify('mail/bill.html.twig', [\n // view params\n ])\n ;\n}\n
"},{"location":"utils/slug/","title":"Slug","text":"Murph requires cocur/slugify. See the official documentation on Github.
"},{"location":"utils/editors/builder/","title":"Builder Block","text":"Its gives you total control to build with blocks.
"},{"location":"utils/editors/builder/#add-the-builder-in-forms","title":"Add the builder in forms","text":""},{"location":"utils/editors/builder/#classic-form","title":"Classic form","text":"src/Form/ExampleType.phpnamespace App\\Form\\ExampleType;\n\nuse App\\Core\\Form\\Type\\BuilderType;\nuse Symfony\\Component\\Form\\AbstractType;\nuse Symfony\\Component\\Form\\FormBuilderInterface;\n\nclass ExampleType extends AbstractType\n{\n public function buildForm(FormBuilderInterface $builder, array $options)\n {\n $builder->add(\n 'myField',\n BuilderType::class\n );\n\n // ...\n }\n\n // ...\n}\n
"},{"location":"utils/editors/builder/#page-form","title":"Page form","text":"src/Entity/Page/YourPage.phpnamespace App\\Entity\\Page;\n\nuse App\\Core\\Entity\\Site\\Page\\Block;\nuse App\\Core\\Entity\\Site\\Page\\BuilderBlock;\nuse App\\Core\\Form\\Site\\Page\\BuilderBlockType;\nuse Symfony\\Component\\Form\\FormBuilderInterface;\n\n#[ORM\\Entity]\nclass YourPage extends Page\n{\n public function buildForm(FormBuilderInterface $builder, array $options)\n {\n $builder->add(\n 'myBlock',\n BuilderBlockType::class,\n [\n 'label' => 'My block',\n 'row_attr' => [\n ],\n 'options' => [\n // options given to the sub form\n ],\n ]\n );\n\n // ...\n }\n\n public function setMyBlock(Block $block)\n {\n return $this->setBlock($block);\n }\n\n public function getMyBlock(): Block\n {\n return $this->getBlock('myBlock', BuilderBlock::class);\n }\n\n // ...\n}\n
Create a template:
templates/page/your_page/default.html.twig{% extends 'base.html.twig' %}\n\n{% block page %}\n {{ _page.myBlock.value|block_to_html }}\n{% endblock %}\n
"},{"location":"utils/editors/builder/#creating-custom-block","title":"Creating custom block","text":"The easy way is to run: php bin/console make:builder-block.
First, create a service which extends App\\Core\\BuilderBlock\\BuilderBlock and tagged builder_block.widget. Then, implement the method configure as below.
src/BuilderBlock/CustomBlock.phpnamespace App\\BuilderBlock;\n\nuse App\\Core\\BuilderBlock\\BuilderBlock;\nuse Symfony\\Component\\DependencyInjection\\Attribute\\AutoconfigureTag;\n\n#[AutoconfigureTag('builder_block.widget')]\nclass CustomBlock extends BuilderBlock\n{\n public function configure()\n {\n $this\n ->setName('custom')\n ->setCategory('Category')\n ->setLabel('My custom block')\n ->setOrder(1)\n ->setIsContainer(false) // set `true` if the block can contain blocks\n ->setIcon('<i class=\"fas fa-pencil-alt\"></i>')\n ->setTemplate('builder_block/custom.html.twig')\n ->setClass('col-md-12')\n ->addSetting(name: 'value', label: 'Value', type: 'textarea', attributes: [], default: 'Default value')\n ;\n }\n}\n
Create a template:
templates/builder_block/custom.html.twig<div id=\"{{ id }}\">\n {{ settings.value|default(null) }}\n\n {# If it's a container: #}\n {% for item in children %}\n {{ item|block_to_html(context) }}\n {% endfor %}\n</div>\n
That's all folks!
"},{"location":"utils/editors/builder/#rendering","title":"Rendering","text":"To render blocks, simply use {{ value|block_to_html }}.
If you need to build variables depending of the content, you can override the method buildVars:
src/BuilderBlock/CustomBlock.phpnamespace App\\BuilderBlock;\n\nuse App\\Core\\BuilderBlock\\BuilderBlock;\nuse Symfony\\Component\\DependencyInjection\\Attribute\\AutoconfigureTag;\n\n#[AutoconfigureTag('builder_block.widget')]\nclass CustomBlock extends BuilderBlock\n{\n // ...\n\n public function buildVars(array $data, array $context)\n {\n $this->vars['bar'] = 'bar';\n }\n}\n
And you can access variables in the template:
templates/builder_block/custom.html.twig{{ vars.bar }}\n
"},{"location":"utils/editors/editorjs/","title":"Editor.js","text":"Workspace in classic editors is made of a single contenteditable element, used to create different HTML markups. Editor.js workspace consists of separate Blocks: paragraphs, headings, images, lists, quotes, etc. Each of them is an independent contenteditable element (or more complex structure) provided by Plugin and united by Editor's Core.
Editor.js is fully integrated in Murph as form types.
"},{"location":"utils/editors/editorjs/#classic-form","title":"Classic form","text":"// src/Form/ExampleType.php\nnamespace App\\Form\\ExampleType;\n\nuse App\\Core\\Form\\Type\\EditorJsTextareaType;\nuse Symfony\\Component\\Form\\AbstractType;\nuse Symfony\\Component\\Form\\FormBuilderInterface;\n\nclass ExampleType extends AbstractType\n{\n public function buildForm(FormBuilderInterface $builder, array $options)\n {\n $builder->add(\n 'myField',\n EditorJsTextareaType::class\n );\n\n // ...\n }\n\n // ...\n}\n
Modified data should return stringified JSON array if empty:
public function getMyField(): string\n{\n if (empty($this->myField)) {\n $this->myField = '[]';\n }\n\n return $this->myField;\n}\n
"},{"location":"utils/editors/editorjs/#page-form","title":"Page form","text":"// src/Entity/Page/YourPage.php\nnamespace App\\Entity\\Page;\n\nuse App\\Core\\Entity\\Site\\Page\\Block;\nuse App\\Core\\Form\\Site\\Page\\EditorJsTextareaBlockType;\n\n#[@ORM\\Entity]\nclass YourPage extends Page\n{\n public function buildForm(FormBuilderInterface $builder, array $options)\n {\n $builder->add(\n 'myBlock',\n EditorJsTextareaBlockType::class,\n [\n 'label' => 'My block',\n 'row_attr' => [\n ],\n 'options' => [\n // options given to the sub form\n ],\n ]\n );\n\n // ...\n }\n\n public function setMyBlock(Block $block)\n {\n return $this->setBlock($block);\n }\n\n public function getMyBlock(): Block\n {\n return $this->getBlock('myBlock');\n }\n\n // ...\n}\n
"},{"location":"utils/editors/editorjs/#rendering","title":"Rendering","text":"Editor.js will generate a JSON which contains blocks.
Supported blocks:
- paragraph
- header
- quote
- delimiter
- warning
- list
- nestedList
- checkList
- table
- code
- raw
- image
- link
To render HTML, the basic way is: {{ value|editorjs_to_html }} If you want to render specific blocks: {{ value|editorjs_to_html(['paragraph', 'header', ...])) }}
Block have default templates stored in vendor/murph/murph-core/src/core/Resources/views/editorjs. They can be simply overridden in config/packages/app.yaml:
core:\n editor_js:\n blocks:\n paragraph: 'path/to/paragraph.html.twig'\n header: 'path/to/header.html.twig'\n
"},{"location":"utils/editors/grapesjs/","title":"GrapesJS","text":"GrapesJS is a web builder which combines different tools and features with the goal to help users to build HTML templates without any knowledge of coding. It's a very good solution to replace the common WYSIWYG editor like TinyMCE.
GrapesJS is fully integrated in Murph as form types.
"},{"location":"utils/editors/grapesjs/#classic-form","title":"Classic form","text":"// src/Form/ExampleType.php\nnamespace App\\Form\\ExampleType;\n\nuse App\\Core\\Form\\Type\\GrapesJsType;\nuse Symfony\\Component\\Form\\AbstractType;\nuse Symfony\\Component\\Form\\FormBuilderInterface;\n\nclass ExampleType extends AbstractType\n{\n public function buildForm(FormBuilderInterface $builder, array $options)\n {\n $builder->add(\n 'myField',\n GrapesJsType::class\n );\n\n // ...\n }\n\n // ...\n}\n
"},{"location":"utils/editors/grapesjs/#page-form","title":"Page form","text":"// src/Entity/Page/YourPage.php\nnamespace App\\Entity\\Page;\n\nuse App\\Core\\Entity\\Site\\Page\\Block;\nuse App\\Core\\Form\\Site\\Page\\GrapesJsBlockType;\n\n#[ORM\\Entity]\nclass YourPage extends Page\n{\n public function buildForm(FormBuilderInterface $builder, array $options)\n {\n $builder->add(\n 'myBlock',\n GrapesJsBlockType::class,\n [\n 'label' => 'My block',\n 'row_attr' => [\n ],\n 'options' => [\n // options given to the sub form\n ],\n ]\n );\n\n // ...\n }\n\n public function setMyBlock(Block $block)\n {\n return $this->setBlock($block);\n }\n\n public function getMyBlock(): Block\n {\n return $this->getBlock('myBlock');\n }\n\n // ...\n}\n
"},{"location":"utils/editors/grapesjs/#options","title":"Options","text":"There are 3 modes:
- Bootstrap 4 (default):
bootstrap4 - Preset webpage:
presetWebpage - Preset newsletter:
presetNewsletter
To specify a mode, you must define the attribute data-grapesjs:
src/Form/ExampleType.php$builder->add(\n 'myField',\n GrapesJsType::class,\n [\n // ...\n\n 'attr' => [\n 'data-grapesjs' => 'bootstrap4',\n ],\n ]\n);\n\n```php-inline title=\"src/Entity/Page/YourPage.php\"\n$builder->add(\n 'myBlock',\n GrapesJsBlockType::class,\n [\n // ...\n\n 'options' => [\n 'attr' => [\n 'data-grapesjs' => 'bootstrap4',\n ],\n ],\n ]\n);\n
"},{"location":"utils/editors/grapesjs/#rendering","title":"Rendering","text":"GrapesJS will generate a JSON which contains HTML and CSS.
- To extract HTML:
{% set html = value|grapesjs_html %} - To extract CSS:
{% set css = value|grapesjs_css %}
Depending of the mode, you will need to import in the app sass file:
- Bootstrap 4:
@import \"~bootstrap/scss/bootstrap.scss\"; - Preset webpage:
@import \"~grapesjs-preset-webpage/dist/grapesjs-preset-webpage.min.css\"; - Preset newsletter:
@import \"~grapesjs-preset-newsletter/dist/grapesjs-preset-newsletter.css\";
"},{"location":"utils/editors/tinymce/","title":"TinyMCE","text":"TinyMCE gives you total control over your rich text editing. It's fully integrated in Murph as form types.
"},{"location":"utils/editors/tinymce/#classic-form","title":"Classic form","text":"src/Form/ExampleType.phpnamespace App\\Form\\ExampleType;\n\nuse App\\Core\\Form\\Type\\TinymceTextareaType;\nuse Symfony\\Component\\Form\\AbstractType;\nuse Symfony\\Component\\Form\\FormBuilderInterface;\n\nclass ExampleType extends AbstractType\n{\n public function buildForm(FormBuilderInterface $builder, array $options)\n {\n $builder->add(\n 'myField',\n TinymceTextareaType::class\n );\n\n // ...\n }\n\n // ...\n}\n
"},{"location":"utils/editors/tinymce/#page-form","title":"Page form","text":"src/Entity/Page/YourPage.phpnamespace App\\Entity\\Page;\n\nuse App\\Core\\Entity\\Site\\Page\\Block;\nuse App\\Core\\Form\\Site\\Page\\TinymceTextareaBlockType;\n\n#[ORM\\Entity]\nclass YourPage extends Page\n{\n public function buildForm(FormBuilderInterface $builder, array $options)\n {\n $builder->add(\n 'myBlock',\n TinymceTextareaBlockType::class,\n [\n 'label' => 'My block',\n 'row_attr' => [\n ],\n 'options' => [\n // options given to the sub form\n ],\n ]\n );\n\n // ...\n }\n\n public function setMyBlock(Block $block)\n {\n return $this->setBlock($block);\n }\n\n public function getMyBlock(): Block\n {\n return $this->getBlock('myBlock');\n }\n\n // ...\n}\n
"},{"location":"utils/editors/tinymce/#options","title":"Options","text":"There are 2 predefined modes:
- Default:
default - Light:
light
To specify a mode, you must define the attribute data-tinymce:
src/Form/ExampleType.php$builder->add(\n 'myField',\n TinymceTextareaType::class,\n [\n // ...\n\n 'attr' => [\n 'data-tinymce' => 'light',\n ],\n ]\n);\n
src/Entity/Page/YourPage.php$builder->add(\n 'myBlock',\n TinymceTextareaBlockType::class,\n [\n // ...\n\n 'options' => [\n 'attr' => [\n 'data-tinymce' => 'light',\n ],\n ],\n ]\n);\n
To custom the editor, see the example below:
assets/js/admin.jsimport '../../vendor/murph/murph-core/src/core/Resources/assets/js/admin.js'\n\nwindow.tinymce.language = 'fr_FR'\n\nwindow.tinymceModes = {\n myCustomMode: {\n plugins: '...',\n menubar: '...',\n toolbar: '...'\n quickbars_selection_toolbar: '...'\n contextmenu: '...'\n templates: [\n {\n title: 'Container',\n description: 'Add a bootstrap container',\n content: '<div class=\"container\"><div class=\"selcontent\"></div></div>'\n }\n // ...\n ],\n content_style: '...'\n }\n}\n
src/Form/ExampleType.php$builder->add(\n 'myField',\n TinymceTextareaType::class,\n [\n // ...\n\n 'attr' => [\n 'data-tinymce' => 'myCustomMode',\n ],\n ]\n);\n
src/Entity/Page/YourPage.php$builder->add(\n 'myBlock',\n TinymceTextareaBlockType::class,\n [\n // ...\n\n 'options' => [\n 'attr' => [\n 'data-tinymce' => 'myCustomMode',\n ],\n ],\n ]\n);\n
"},{"location":"utils/editors/tinymce/#rendering","title":"Rendering","text":"TinyMCE generates HTML. To render, simply use {{ value|raw }}.
"},{"location":"utils/form/collection/","title":"Collection","text":"When you need to manage a collection in a form, you can use App\\Core\\Form\\Type\\CollectionType with these options:
Option Type Default Description collection_name string null Unique name of the collection label_add string Add Unique name of the collection label_delete string Delete Unique name of the collection template_before_item string null A template included before an existing itam of the collection template_after_item string null A template included after an existing itam of the collection ...and and all options of a symfony collection type.
"},{"location":"utils/form/file_picker/","title":"File picker","text":"Murph provides a file picker using the file manager system: App\\Core\\Form\\FileManager\\FilePickerType. It allows you to pick an existing file or upload it. After saving, the file is previewed if possible.
"}]}
\ No newline at end of file
diff --git a/sitemap.xml b/sitemap.xml
index 387957b..16b1aac 100644
--- a/sitemap.xml
+++ b/sitemap.xml
@@ -2,182 +2,182 @@
https://doc.murph-project.org/
- 2025-11-28
+ 2025-12-22
daily
https://doc.murph-project.org/abtesting/
- 2025-11-28
+ 2025-12-22
daily
https://doc.murph-project.org/controller/
- 2025-11-28
+ 2025-12-22
daily
https://doc.murph-project.org/procedure/
- 2025-11-28
+ 2025-12-22
daily
https://doc.murph-project.org/requirements/
- 2025-11-28
+ 2025-12-22
daily
https://doc.murph-project.org/sources/
- 2025-11-28
+ 2025-12-22
daily
https://doc.murph-project.org/tasks/
- 2025-11-28
+ 2025-12-22
daily
https://doc.murph-project.org/template/
- 2025-11-28
+ 2025-12-22
daily
https://doc.murph-project.org/users/
- 2025-11-28
+ 2025-12-22
daily
https://doc.murph-project.org/changelog/core/
- 2025-11-28
+ 2025-12-22
daily
https://doc.murph-project.org/changelog/skeleton/
- 2025-11-28
+ 2025-12-22
daily
https://doc.murph-project.org/crud/
- 2025-11-28
+ 2025-12-22
daily
https://doc.murph-project.org/crud/configuration/
- 2025-11-28
+ 2025-12-22
daily
https://doc.murph-project.org/crud/generator/
- 2025-11-28
+ 2025-12-22
daily
https://doc.murph-project.org/entities/em/
- 2025-11-28
+ 2025-12-22
daily
https://doc.murph-project.org/entities/factory/
- 2025-11-28
+ 2025-12-22
daily
https://doc.murph-project.org/entities/query/
- 2025-11-28
+ 2025-12-22
daily
https://doc.murph-project.org/settings/global/
- 2025-11-28
+ 2025-12-22
daily
https://doc.murph-project.org/settings/navigation/
- 2025-11-28
+ 2025-12-22
daily
https://doc.murph-project.org/tree/
- 2025-11-28
+ 2025-12-22
daily
https://doc.murph-project.org/tree/menu/
- 2025-11-28
+ 2025-12-22
daily
https://doc.murph-project.org/tree/navigation/
- 2025-11-28
+ 2025-12-22
daily
https://doc.murph-project.org/tree/node/
- 2025-11-28
+ 2025-12-22
daily
https://doc.murph-project.org/tree/page/
- 2025-11-28
+ 2025-12-22
daily
https://doc.murph-project.org/utils/cache/
- 2025-11-28
+ 2025-12-22
daily
https://doc.murph-project.org/utils/doctrine/
- 2025-11-28
+ 2025-12-22
daily
https://doc.murph-project.org/utils/file_attribute/
- 2025-11-28
+ 2025-12-22
daily
https://doc.murph-project.org/utils/file_handler/
- 2025-11-28
+ 2025-12-22
daily
https://doc.murph-project.org/utils/mail/
- 2025-11-28
+ 2025-12-22
daily
https://doc.murph-project.org/utils/slug/
- 2025-11-28
+ 2025-12-22
daily
https://doc.murph-project.org/utils/editors/builder/
- 2025-11-28
+ 2025-12-22
daily
https://doc.murph-project.org/utils/editors/editorjs/
- 2025-11-28
+ 2025-12-22
daily
https://doc.murph-project.org/utils/editors/grapesjs/
- 2025-11-28
+ 2025-12-22
daily
https://doc.murph-project.org/utils/editors/tinymce/
- 2025-11-28
+ 2025-12-22
daily
https://doc.murph-project.org/utils/form/collection/
- 2025-11-28
+ 2025-12-22
daily
https://doc.murph-project.org/utils/form/file_picker/
- 2025-11-28
+ 2025-12-22
daily
\ No newline at end of file
diff --git a/sitemap.xml.gz b/sitemap.xml.gz
index 7f26526..fbac70c 100644
Binary files a/sitemap.xml.gz and b/sitemap.xml.gz differ
diff --git a/utils/mail/index.html b/utils/mail/index.html
index f0a6f38..b241938 100644
--- a/utils/mail/index.html
+++ b/utils/mail/index.html
@@ -1681,7 +1681,7 @@
->init()
->setSubject('Your bill')
->addRecipient('john.doe@example.com')
- ->addRecipients($query->create()->where('.isAdmin = true')->find(), true)
+ ->addRecipientsByUsers($query->create()->where('.isAdmin = true')->find(), true)
->addAttachment('path/to/bill.pdf')
->notify('mail/bill.html.twig', [
// view params