diff --git a/changelog/core/index.html b/changelog/core/index.html index 28b4f7a..0f1910e 100644 --- a/changelog/core/index.html +++ b/changelog/core/index.html @@ -1583,6 +1583,19 @@
Added
+no-wrap
Fixed
+Added
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:
App\\Core\\Ab\\AbTestInterface
objectGo the navigation and edit the tested node:
example_test
)The event subscriber helps you to define each variation and the TTL.
src/EventSubscriber/MyAbTestEventSubscriber.phpnamespace App\\EventSubscriber;\nuse App\\Core\\EventSubscriber\\AbEventSubscriber as EventSubscriber;\nuse App\\Core\\Event\\Ab\\AbTestEvent;\nclass MyAbTestEventSubscriber extends EventSubscriber\n{\npublic function onInit(AbTestEvent $event)\n{\nif ($event->getTest()->getName() !== 'example_test') {\nreturn;\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}\npublic 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;\npublic function foo(AbContainerInterface $testContainer)\n{\nif ($testContainer->has('example_test')) {\n$test = $testContainer->get('example_test');\n$result = $test->getResult(); // eg: \"test_2\"\n$value = $test->getResultValue(); // eg: \"Value #2\"\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{% 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;\nuse App\\Core\\EventListener\\AbListener as EventListener;\nuse Symfony\\Component\\HttpFoundation\\Request;\nuse Symfony\\Component\\HttpKernel\\Event\\RequestEvent;\nclass CustomAbListener extends EventListener\n{\n/**\n * {@inheritdoc}\n */\nprotected function supports(Request $request): bool\n{\nreturn true;\n}\n/**\n * {@inheritdoc}\n */\nprotected function getAbTestCode(): string\n{\nreturn 'my_global_ab_test_code';\n}\n}\n
CustomAbListener
must be registred:
services:\n# ...\nApp\\EventListener\\CustomAbListener;\ntags:\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;\nuse App\\Core\\Controller\\Site\\PageController;\nclass MyController extends PageController\n{\npublic function myAction()\n{\nif (!$this->siteRequest->getPage()) {\nthrow $this->createNotFoundException();\n}\nreturn $this->defaultRender($this->siteRequest->getPage()->getTemplate(), [\n// view datas\n]);\n}\n}\n
Then edit config/packages/app.yaml
and add your controller:
core:\nsite:\ncontrollers:\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.
namespace App\\UrlGenerator;\nuse App\\Core\\Entity\\Site\\Node;\nuse App\\Repository\\MyEntityRepositoryQuery;\nuse Symfony\\Component\\Routing\\Generator\\UrlGeneratorInterface;\nclass MyEntityGenerator\n{\nprotected MyEntityRepositoryQuery $query;\nprotected UrlGeneratorInterface $urlGenerator;\npublic function __construct(MyEntityRepositoryQuery $query, UrlGeneratorInterface $urlGenerator)\n{\n$this->query = $query;\n$this->urlGenerator = $urlGenerator;\n}\npublic function myActionGenerator(Node $node, array $options): array\n{\n$entities = $this->query->create()->find();\n$urls = [];\nforeach ($entities as $entity) {\n$urls[] = $this->urlGenerator->generate(\n$node->getRouteName(),\n[\n'entity' => $entity->getId(),\n'_domain' => $options['_domain'],\n],\nUrlGeneratorInterface::ABSOLUTE_URL\n);\n}\nreturn $urls;\n}\n}\n
Then, the have to annotate the controller like this (note: options
is optional):
namespace App\\Controller;\nuse App\\Core\\Annotation\\UrlGenerator;\nuse App\\Core\\Controller\\Site\\PageController;\nuse App\\UrlGenerator\\MyEntityGenerator;\nclass MyController extends PageController\n{\n#[UrlGenerator(service: MyEntityGenerator::class, method: 'myActionGenerator', options=[])]\npublic function myAction(MyEntity $entity)\n{\n// do stuff\n}\n}\n
Finally, update config/services.yaml
:
services:\n# ...\nApp\\UrlGenerator\\MyEntityGenerator:\npublic: 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.
.env
into .env.local
.env.local
(documentation). Don't forget to set APP_SECRET
.make build
Run \"$PHP_BIN\" bin/console murph:user:create
and answer questions.
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":"make
Source codes are accessible on Gitnet:
Tasks are scripts executabled from UI. The creation of tasks is based on events.
src/EventSubscriber/MyTaskEventSubscriber.phpnamespace App\\EventSubscriber;\nuse App\\Core\\Event\\Task\\TaskInitEvent;\nuse App\\Core\\Event\\Task\\TaskRunRequestedEvent;\nuse App\\Core\\EventSubscriber\\Task\\TaskEventSubscriber;\nclass MyTaskEventSubscriber extends TaskEventSubscriber\n{\npublic function onInit(TaskInitEvent $event)\n{\n$event->addTask('my_task', 'Example', 'My task');\n}\npublic function onRunRequest(TaskRunRequestedEvent $event)\n{\nif ('my_task' !== $event->getTask()) {\nreturn;\n}\n$event->getOutput()->writeln('My task is started');\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:
_node
and its menu is _menu
_navigation
_locale
_store
_store.navigations
Retrieve a navigation by its code: _store.navigation('the_code')
Retrieve all navigation menus: _navigation.menus
_navigation.menu('the_code')
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)
_store.isActiveNode(node, true)
You can access a page's blocks this way:
{% set myBlock = _page.myBlock.value %}\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":"{{ safe_url(routeName, options, relative) }}
{{ safe_path(routeName, options, relative) }}
{{ node_url(node, options, relative) }}
{{ node_path(node, options, relative) }}
{{ safe_node_url(node, options, relative) }}
{{ 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":"{{ code_url(menuCode, nodeCode, options, relative) }}
{{ code_path(menuCode, nodeCode, options, relative) }}
{{ safe_code_url(menuCode, nodeCode, options, relative) }}
{{ safe_code_path(menuCode, nodeCode, options, relative) }}
When a content could contains tags (eg: '{{url://my_route}}), use
murph_url`. See the example below:
{{ 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.
Added
Length
in formsChanged
Fixed
Added
toggle|checkbox_class_when_true
and toggle|checkbox_class_when_false
count
method in repository queryaddForcedFilterHandler
method in repository queryinline_form_validation
option to validate inline forms with custom algoAdded
Fixed
Added
default_value
option in crud fieldsdisplay
option in BooleanFieldFixed
Added
Fixed
Changed
Added
action
in field to add a link to the view page or to the edition pageinline_form
in field to configure to edit the datasetDoubleClick
in the crud configurationAdded
Fixed
Fixed
Fixed
Changed
Added
Fixed
Changed
Added
Fixed
Changed
entity_to_array
Added
Fixed
Added
Changed
Added
Fixed
Changed
Added
make:page
)Fixed
Changed
Added
Changed
Added
Changed
Fixed
Added
Changed
Added
Changed
Added
Changed
core/EventSuscriber
with core/EventSubscriber
Added
Fixed
Added
Fixed
Added
Fixed
Fixed
Changed
Added
Fixed
Changed
Added
Changed
Added
Fixed
Changed
Added
Added
Fixed
Added
Changed
Added
Fixed
Changed
Fixed
Changed
Added
Fixed
Changed
Changed
Fixed
Added
Changed
Changed
Added
Fixed
Changed
Added
Fixed
Changed
Changed
Added
Changed
Fixed
Changed
Changed
Changed
Changed
Added
Fixed
Changed
Added
Changed
Added
Changed
Added
Changed
core/EventSuscriber
with core/EventSubscriber
Added
Fixed
Added
Fixed
Added
Fixed
Fixed
Changed
Added
Fixed
Changed
Added
Changed
Added
Fixed
Changed
Added
Added
Fixed
Added
Changed
Added
Fixed
Changed
Fixed
Murph helps you to manage specific entities with a CRUD manager:
You can configure almost anything:
A generated crud controller contains a method named getConfiguration
. This methods returns a instance of App\\Core\\Crud\\CrudConfiguration
.
setPageTitle(string $page, string $title)
Set the title of the given page.
Example of usage in a CRUD template: <title>{{ configuration.pageTitle('index') }}</title>
.
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>
.
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 %}
.
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 }}
setView(string $context, string $view)
Override a view.
Controller (context) View Descriptionindex
@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/#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 Descriptionproperty
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) {\nreturn $entity->getMyProperty();\n},\n])\n
Example #1$configuration->setField('index', 'My field', TextField::class, [\n'raw' => true,\n'property_builder' => function($entity, array $options) {\nreturn 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$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
view
string
@Core/admin/crud/field/text.html.twig
The templated rendered"},{"location":"crud/configuration/#datefield","title":"DateField","text":"App\\Core\\Crud\\Field\\DateField
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
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
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
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
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 * @ORM\\Column(type=\"integer\", nullable=true)\n */\nprivate $sortOrder;\npublic function getSortOrder(): ?int\n{\nreturn $this->sortOrder;\n}\npublic function setSortOrder(?int $sortOrder): self\n{\n$this->sortOrder = $sortOrder;\nreturn $this;\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.
setBatchAction(string $context, string $action, string $label, callable $callack)
Add a batch action. The callback has 2 arguments:
App\\Core\\Entity\\EntityInterface
App\\Core\\Manager\\EntityManager
use App\\Core\\Entity\\EntityInterface;\nuse App\\Core\\Manager\\EntityManager;\n$configuration->setBatchAction(\n'index',\n'delete',\n'Delete',\nfunction(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:
App\\Core\\Repository\\RepositoryQuery
App\\Core\\Manager\\EntityManager
null
valueDo 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$configuration->setGlobalBatchAction(\n'index',\n'export_json',\n'Export to JSON',\nfunction(RepositoryQuery $query, EntityManager $manager, ?array $selection): JsonResponse {\n$items = $selection ?? $query->find();\nreturn $this->json($items);\n}\n);\n
"},{"location":"crud/generator/","title":"Generator","text":""},{"location":"crud/generator/#prepare-your-entity","title":"Prepare your entity","text":"App\\Core\\Entity\\EntityInterface
(see Entity Manager)php bin/console make:form --help
)The generation is performed in CLI. These information are required:
MyEntityAdminController
)MyEntity
)MyEntityRepositoryQuery
)MyEntityFactory
)MyEntityType
)Simply run php bin/console make:crud-controller
.
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
.
namespace App\\Entity;\nuse App\\Repository\\MyEntityRepository;\nuse Doctrine\\ORM\\Mapping as ORM;\nuse App\\Core\\Entity\\EntityInterface;\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 entitiesApp\\Core\\Manager\\TranslatableEntityManager
used for translatable entitiesnamespace App\\Controller;\nuse App\\Core\\Manager\\EntityManager\nuse App\\Entity\\MyEntity;\nuse Symfony\\Component\\HttpFoundation\\Response;\nclass FooController\n{\npublic function foo(EntityManager $entityManager): Response\n{\n$myEntity = new MyEntity();\n// Creates an entity\n$entityManager->create($myEntity);\n// Updates an entity\n$entityManager->update($myEntity);\n// Deletes an entity\n$entityManager->delete($myEntity);\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;\nuse App\\Core\\Entity\\EntityInterface;\nuse App\\Core\\Event\\EntityManager\\EntityManagerEvent;\nuse App\\Core\\EventSubscriber\\EntityManagerEventSubscriber;\nuse App\\Entity\\MyEntity;\nclass MyEntityEventSubscriber extends EntityManagerEventSubscriber\n{\npublic function supports(EntityInterface $entity): bool\n{\nreturn $entity instanceof MyEntity;\n}\npublic function onCreate(EntityManagerEvent $event)\n{\nif (!$this->supports($event->getEntity())) {\nreturn;\n}\n// ...\n}\npublic function onUpdate(EntityManagerEvent $event)\n{\nif (!$this->supports($event->getEntity())) {\nreturn;\n}\n// ...\n}\npublic function onDelete(EntityManagerEvent $event)\n{\nif (!$this->supports($event->getEntity())) {\nreturn;\n}\n// ...\n}\npublic function onPreCreate(EntityManagerEvent $event)\n{\nif (!$this->supports($event->getEntity())) {\nreturn;\n}\n// ...\n}\npublic function onPreUpdate(EntityManagerEvent $event)\n{\nif (!$this->supports($event->getEntity())) {\nreturn;\n}\n// ...\n}\npublic function onPreDelete(EntityManagerEvent $event)\n{\nif (!$this->supports($event->getEntity())) {\nreturn;\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
.
A factory is basically a service which contain at lease a method named create
.
The generation is performed in CLI. These information are required:
MyEntityFactory
)MyEntity
)Simply run php bin/console make:factory
.
namespace App\\Controller;\nuse App\\Factory\\MyEntityFactory;\nuse Symfony\\Component\\HttpFoundation\\Response;\nclass FooController\n{\npublic function foo(MyEntityFactory $factory): Response\n{\n$entity = $factory->create();\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
.
namespace App\\Entity;\nuse App\\Repository\\MyEntityRepository;\nuse Doctrine\\ORM\\Mapping as ORM;\nuse App\\Core\\Entity\\EntityInterface;\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:
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;\nuse App\\Core\\Repository\\RepositoryQuery;\nuse Knp\\Component\\Pager\\PaginatorInterface;\nclass MyEntityRepositoryQuery extends RepositoryQuery\n{\npublic function __construct(MyEntityRepository $repository, PaginatorInterface $paginator)\n{\nparent::__construct($repository, 'm', $paginator);\n}\n// Example of custom filter\npublic 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);\nreturn $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;\nuse App\\Repository\\MyEntityRepositoryQuery\nuse Symfony\\Component\\HttpFoundation\\Response;\nclass FooController\n{\npublic function foo(MyEntityRepositoryQuery $query): Response\n{\n$entities = $query->create()->find();\n$entity = $query->create()->findOne();\n// Filtered and sorted entities\n$entities = $query->create()\n->orderBy('.id', 'DESC')\n->where('.isPublished = true')\n->find();\n// ...\n}\n}\n
"},{"location":"entities/query/#custom-methods","title":"Custom methods","text":"// ...\nclass MyEntityRepositoryQuery extends RepositoryQuery\n{\n// ...\npublic 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);\nreturn $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:
// ...\nuse App\\Entity\\Something;\nclass MyEntityRepositoryQuery extends RepositoryQuery\n{\n// ...\npublic function filterHandler(string $name, $value): self\n{\nif ($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:
// ...\nclass MyEntityRepositoryQuery extends RepositoryQuery\n{\npublic function __construct(Repository $repository, PaginatorInterface $paginator)\n{\n// ...\n$this->addForcedFilterHandler('foo');\n}\npublic function filterHandler(string $name, $value): self\n{\n// ...\nif ($name === 'foo) {\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;\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;\nclass SettingEventSubscriber extends EventSubscriber\n{\nprotected SettingManager $manager;\npublic function __construct(SettingManager $manager)\n{\n$this->manager = $manager;\n}\npublic 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}\npublic function onFormInit(SettingEvent $event)\n{\n$data = $event->getData();\n$builder = $data['builder'];\n$entity = $data['entity'];\nif (in_array($entity->getCode(), ['app_font_color', 'app_background_color'])) {\n$builder->add(\n'value',\nColorType::class,\n[\n'label' => $entity->getLabel(),\n]\n);\n}\nif (in_array($entity->getCode(), ['app_maintenance_enabled'])) {\n$builder->add(\n'value',\nCheckboxType::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.
namespace App\\Controller;\nuse App\\Core\\Setting\\SettingManager;\nuse Symfony\\Component\\HttpFoundation\\Response;\nclass FooController\n{\npublic 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
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.
namespace App\\Controller;\nuse App\\Core\\Setting\\SettingManager;\nuse Symfony\\Component\\HttpFoundation\\Response;\nclass FooController\n{\npublic 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
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 modalThe 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;\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;\nclass NavigationSettingEventSubscriber extends EventSubscriber\n{\nprotected NavigationSettingManager $manager;\npublic function __construct(NavigationSettingManager $manager)\n{\n$this->manager = $manager;\n}\npublic function onInit(NavigationSettingEvent $event)\n{\n$data = $event->getData();\n$navigation = $data['navigation'];\n$this->manager->init($navigation, 'nav_tracker_code', 'Stats', 'Tracker', '');\n$this->manager->init($navigation, 'nav_contact_email', 'Contact', 'Email', 'foo@example.com');\n}\npublic function onFormInit(NavigationSettingEvent $event)\n{\n$data = $event->getData();\n$builder = $data['builder'];\n$entity = $data['entity'];\nif (in_array($entity->getCode(), ['nav_tracker_code'])) {\n$builder->add(\n'value',\nTextType::class,\n[\n'label' => $entity->getLabel(),\n]\n);\n}\nif (in_array($entity->getCode(), ['nav_contact_email'])) {\n$builder->add(\n'value',\nEmailType::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.
namespace App\\Controller;\nuse App\\Core\\Setting\\NavigationSettingManager;\nuse App\\Core\\Site\\SiteRequest;\nuse Symfony\\Component\\HttpFoundation\\Response;\nclass FooController\n{\npublic 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
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.
namespace App\\Controller;\nuse App\\Core\\Setting\\NavigationSettingManager;\nuse App\\Core\\Site\\SiteRequest;\nuse Symfony\\Component\\HttpFoundation\\Response;\nclass FooController\n{\npublic 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
You can also edit them from UI:
"},{"location":"tree/","title":"Tree manager","text":"Murph manages contents this way:
%%{\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.
label
is the label displayed whenever necessary (eg: Top menu
)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.
label
is the label displayed whenever necessary (eg: Example
)locale
is the language used in the content (eg: en
)code
is a unique technical identifier useful in templating, routing and settings (eg: example_en
)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 wantIf 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:
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.
The routing tab is very important. It allows you to define all parameters related to:
code
To add a controller in the list, edit config/packages/app.yaml
:
core:\nsite:\ncontrollers:\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:\nsite:\nsecurity:\nroles:\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.
namespace App\\Entity\\Page;\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#[ORM\\Entity]\nclass YourPage extends Page\n{\npublic function buildForm(FormBuilderInterface $builder, array $options)\n{\n$builder->add(\n'myBlock',\nTextBlockType::class,\n[\n'label' => 'My block',\n'options' => [\n// options given to the sub form\n],\n]\n);\n// ...\n}\npublic function setMyBlock(Block $block)\n{\nreturn $this->setBlock($block);\n}\npublic function getMyBlock(): Block\n{\nreturn $this->getBlock('myBlock');\n}\n// ...\n}\n
Then edit config/packages/app.yaml
and add your page:
core:\nsite:\npages:\nApp\\Entity\\Page\\SimplePage:\nname: 'Simple page'\ntemplates:\n- {name: \"Default\", file: \"page/simple/default.html.twig\"}\nApp\\Entity\\Page\\YourPage:\nname: 'Your page'\ntemplates:\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
.
App\\Core\\Form\\Site\\Page\\TextareaBlockType
will render a symfony TextareaType
.
App\\Core\\Form\\Site\\Page\\ChoiceBlockType
will render a symfony ChoiceType
.
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;\npublic function getMyBlock(): Block\n{\nreturn $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.
App\\Core\\Form\\Site\\Page\\EditorJsTextareaBlockType
will render a EditorJs widget.
App\\Core\\Form\\Site\\Page\\GrapesJsBlockType
will render a GrapesJS editor.
App\\Core\\Form\\Site\\Page\\TinymceTextareaBlockType
will render a Tinymce editor.
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;\npublic function getMyBlock(): Block\n{\nreturn $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;\npublic function getMyBlock(): Block\n{\nreturn $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;\nuse App\\Core\\Event\\Page\\PageEditEvent;\nuse App\\Entity\\Page\\YourPage;\nclass PageEventSubscriber implements EventSubscriberInterface\n{\npublic static function getSubscribedEvents()\n{\nreturn [\nPageEditEvent::FORM_INIT_EVENT => ['onFormInit'],\n];\n}\npublic function onFormInit(PageEditEvent $event)\n{\nif ($event->getPage() instanceof YourPage) {\n$event->addPageBuilderOptions([\n// options\n]);\n}\n}\n}\n
"},{"location":"utils/cache/","title":"Cache Manager","text":"App\\Core\\Cache\\SymfonyCacheManager
cleanRouting()
: clear the cache of routescleanAll(OutputInterface $output = null)
: clean and warmup all cacheApp\\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.
namespace App/Entity;\nuse use App\\Core\\Entity\\EntityInterface;\nuse Doctrine\\ORM\\Mapping as ORM;\nuse App\\Core\\Doctrine\\Timestampable;\nuse App\\Core\\Entity\\EntityInterface;\nuse App\\Repository\\FooRepository;\nuse Doctrine\\ORM\\Mapping as ORM;\n#[ORM\\Entity(repositoryClass: FooRepository::class)]\n#[ORM\\HasLifecycleCallbacks]\nclass FooEntity implements EntityInterface\n{\nuse Timestampable;\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$path = 'path/to/file';\n$file = FileAttribute::handleFile($path); // returns an instance of File\n$path = 'path/to/file';\n$file = FileAttribute::handleFile($path, Bar::class); // returns an instance of Bar\n$path = 'path/to/removed_file';\n$file = FileAttribute::handleFile($path); // returns null\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;\npublic function upload(Request $request, FileUploadHandler $fileUpload)\n{\n$entity = new Foo();\n$form = $this->createForm(FooType::class, $foo);\nif ($request->isMethod('POST')) {\n$form->handleRequest($request);\nif ($form->isValid()) {\n$fileDirectory = 'uploads/';\n$keepOriginalFilename = false;\n$fileUpload->handleForm(\nuploadedFile: $form->get('image')->getData(), // Symfony\\Component\\HttpFoundation\\File\\UploadedFile or null\npath: $fileDirectory,\n// optional\nafterUploadCallback: function ($filename) use ($entity, $fileDirectory) {\n$entity->setImage($fileDirectory.$filename);\n},\n// optional\nkeepOriginalFilename: $keepOriginalFilename\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;\npublic function foo(MailNotifier $notifier, UserRepositoryQuery $query)\n{\n// ...\n$notifier\n->init()\n->setSubject('Your bill')\n->addRecipient('john.doe@example.com')\n->addRecipients(array_map(\nfn($u) => $u->getEmail(),\n$query->create()->where('.isAdmin = true')->find()\n), 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.
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;\nuse App\\Core\\Form\\Type\\EditorJsTextareaType;\nuse Symfony\\Component\\Form\\AbstractType;\nuse Symfony\\Component\\Form\\FormBuilderInterface;\nclass ExampleType extends AbstractType\n{\npublic function buildForm(FormBuilderInterface $builder, array $options)\n{\n$builder->add(\n'myField',\nEditorJsTextareaType::class\n);\n// ...\n}\n// ...\n}\n
Modified data should return stringified JSON array if empty:
public function getMyField(): string\n{\nif (empty($this->myField)) {\n$this->myField = '[]';\n}\nreturn $this->myField;\n}\n
"},{"location":"utils/editors/editorjs/#page-form","title":"Page form","text":"// src/Entity/Page/YourPage.php\nnamespace App\\Entity\\Page;\nuse App\\Core\\Entity\\Site\\Page\\Block;\nuse App\\Core\\Form\\Site\\Page\\EditorJsTextareaBlockType;\n#[@ORM\\Entity]\nclass YourPage extends Page\n{\npublic function buildForm(FormBuilderInterface $builder, array $options)\n{\n$builder->add(\n'myBlock',\nEditorJsTextareaBlockType::class,\n[\n'label' => 'My block',\n'row_attr' => [\n],\n'options' => [\n// options given to the sub form\n],\n]\n);\n// ...\n}\npublic function setMyBlock(Block $block)\n{\nreturn $this->setBlock($block);\n}\npublic function getMyBlock(): Block\n{\nreturn $this->getBlock('myBlock');\n}\n// ...\n}\n
"},{"location":"utils/editors/editorjs/#rendering","title":"Rendering","text":"Editor.js will generate a JSON which contains blocks.
Supported blocks:
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:\neditor_js:\nblocks:\nparagraph: 'path/to/paragraph.html.twig'\nheader: '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;\nuse App\\Core\\Form\\Type\\GrapesJsType;\nuse Symfony\\Component\\Form\\AbstractType;\nuse Symfony\\Component\\Form\\FormBuilderInterface;\nclass ExampleType extends AbstractType\n{\npublic function buildForm(FormBuilderInterface $builder, array $options)\n{\n$builder->add(\n'myField',\nGrapesJsType::class\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;\nuse App\\Core\\Entity\\Site\\Page\\Block;\nuse App\\Core\\Form\\Site\\Page\\GrapesJsBlockType;\n#[ORM\\Entity]\nclass YourPage extends Page\n{\npublic function buildForm(FormBuilderInterface $builder, array $options)\n{\n$builder->add(\n'myBlock',\nGrapesJsBlockType::class,\n[\n'label' => 'My block',\n'row_attr' => [\n],\n'options' => [\n// options given to the sub form\n],\n]\n);\n// ...\n}\npublic function setMyBlock(Block $block)\n{\nreturn $this->setBlock($block);\n}\npublic function getMyBlock(): Block\n{\nreturn $this->getBlock('myBlock');\n}\n// ...\n}\n
"},{"location":"utils/editors/grapesjs/#options","title":"Options","text":"There are 3 modes:
bootstrap4
presetWebpage
presetNewsletter
To specify a mode, you must define the attribute data-grapesjs
:
$builder->add(\n'myField',\nGrapesJsType::class,\n[\n// ...\n'attr' => [\n'data-grapesjs' => 'bootstrap4',\n],\n]\n);\n```php-inline title=\"src/Entity/Page/YourPage.php\"\n$builder->add(\n'myBlock',\nGrapesJsBlockType::class,\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.
{% set html = value|grapesjs_html %}
{% set css = value|grapesjs_css %}
Depending of the mode, you will need to import in the app sass file:
@import \"~bootstrap/scss/bootstrap.scss\";
@import \"~grapesjs-preset-webpage/dist/grapesjs-preset-webpage.min.css\";
@import \"~grapesjs-preset-newsletter/dist/grapesjs-preset-newsletter.css\";
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;\nuse App\\Core\\Form\\Type\\TinymceTextareaType;\nuse Symfony\\Component\\Form\\AbstractType;\nuse Symfony\\Component\\Form\\FormBuilderInterface;\nclass ExampleType extends AbstractType\n{\npublic function buildForm(FormBuilderInterface $builder, array $options)\n{\n$builder->add(\n'myField',\nTinymceTextareaType::class\n);\n// ...\n}\n// ...\n}\n
"},{"location":"utils/editors/tinymce/#page-form","title":"Page form","text":"src/Entity/Page/YourPage.phpnamespace App\\Entity\\Page;\nuse App\\Core\\Entity\\Site\\Page\\Block;\nuse App\\Core\\Form\\Site\\Page\\TinymceTextareaBlockType;\n#[ORM\\Entity]\nclass YourPage extends Page\n{\npublic function buildForm(FormBuilderInterface $builder, array $options)\n{\n$builder->add(\n'myBlock',\nTinymceTextareaBlockType::class,\n[\n'label' => 'My block',\n'row_attr' => [\n],\n'options' => [\n// options given to the sub form\n],\n]\n);\n// ...\n}\npublic function setMyBlock(Block $block)\n{\nreturn $this->setBlock($block);\n}\npublic function getMyBlock(): Block\n{\nreturn $this->getBlock('myBlock');\n}\n// ...\n}\n
"},{"location":"utils/editors/tinymce/#options","title":"Options","text":"There are 2 predefined modes:
default
light
To specify a mode, you must define the attribute data-tinymce
:
$builder->add(\n'myField',\nTinymceTextareaType::class,\n[\n// ...\n'attr' => [\n'data-tinymce' => 'light',\n],\n]\n);\n
src/Entity/Page/YourPage.php$builder->add(\n'myBlock',\nTinymceTextareaBlockType::class,\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'\nwindow.tinymce.language = 'fr_FR'\nwindow.tinymce.murph.modes.myCustomMode = {\nplugins: '...',\nmenubar: '...',\ntoolbar: '...'\nquickbars_selection_toolbar: '...'\ncontextmenu: '...'\ntemplates: [\n{\ntitle: 'Container',\ndescription: 'Add a bootstrap container',\ncontent: '<div class=\"container\"><div class=\"selcontent\"></div></div>'\n}\n// ...\n],\ncontent_style: '...'\n}\n
src/Form/ExampleType.php$builder->add(\n'myField',\nTinymceTextareaType::class,\n[\n// ...\n'attr' => [\n'data-tinymce' => 'myCustomMode',\n],\n]\n);\n
src/Entity/Page/YourPage.php$builder->add(\n'myBlock',\nTinymceTextareaBlockType::class,\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 }}
.
When you need to manage a collection in a form, you can use App\\Core\\Form\\Type\\CollectionType
with these options:
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.
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:
App\\Core\\Ab\\AbTestInterface
objectGo the navigation and edit the tested node:
example_test
)The event subscriber helps you to define each variation and the TTL.
src/EventSubscriber/MyAbTestEventSubscriber.phpnamespace App\\EventSubscriber;\nuse App\\Core\\EventSubscriber\\AbEventSubscriber as EventSubscriber;\nuse App\\Core\\Event\\Ab\\AbTestEvent;\nclass MyAbTestEventSubscriber extends EventSubscriber\n{\npublic function onInit(AbTestEvent $event)\n{\nif ($event->getTest()->getName() !== 'example_test') {\nreturn;\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}\npublic 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;\npublic function foo(AbContainerInterface $testContainer)\n{\nif ($testContainer->has('example_test')) {\n$test = $testContainer->get('example_test');\n$result = $test->getResult(); // eg: \"test_2\"\n$value = $test->getResultValue(); // eg: \"Value #2\"\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{% 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;\nuse App\\Core\\EventListener\\AbListener as EventListener;\nuse Symfony\\Component\\HttpFoundation\\Request;\nuse Symfony\\Component\\HttpKernel\\Event\\RequestEvent;\nclass CustomAbListener extends EventListener\n{\n/**\n * {@inheritdoc}\n */\nprotected function supports(Request $request): bool\n{\nreturn true;\n}\n/**\n * {@inheritdoc}\n */\nprotected function getAbTestCode(): string\n{\nreturn 'my_global_ab_test_code';\n}\n}\n
CustomAbListener
must be registred:
services:\n# ...\nApp\\EventListener\\CustomAbListener;\ntags:\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;\nuse App\\Core\\Controller\\Site\\PageController;\nclass MyController extends PageController\n{\npublic function myAction()\n{\nif (!$this->siteRequest->getPage()) {\nthrow $this->createNotFoundException();\n}\nreturn $this->defaultRender($this->siteRequest->getPage()->getTemplate(), [\n// view datas\n]);\n}\n}\n
Then edit config/packages/app.yaml
and add your controller:
core:\nsite:\ncontrollers:\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.
namespace App\\UrlGenerator;\nuse App\\Core\\Entity\\Site\\Node;\nuse App\\Repository\\MyEntityRepositoryQuery;\nuse Symfony\\Component\\Routing\\Generator\\UrlGeneratorInterface;\nclass MyEntityGenerator\n{\nprotected MyEntityRepositoryQuery $query;\nprotected UrlGeneratorInterface $urlGenerator;\npublic function __construct(MyEntityRepositoryQuery $query, UrlGeneratorInterface $urlGenerator)\n{\n$this->query = $query;\n$this->urlGenerator = $urlGenerator;\n}\npublic function myActionGenerator(Node $node, array $options): array\n{\n$entities = $this->query->create()->find();\n$urls = [];\nforeach ($entities as $entity) {\n$urls[] = $this->urlGenerator->generate(\n$node->getRouteName(),\n[\n'entity' => $entity->getId(),\n'_domain' => $options['_domain'],\n],\nUrlGeneratorInterface::ABSOLUTE_URL\n);\n}\nreturn $urls;\n}\n}\n
Then, the have to annotate the controller like this (note: options
is optional):
namespace App\\Controller;\nuse App\\Core\\Annotation\\UrlGenerator;\nuse App\\Core\\Controller\\Site\\PageController;\nuse App\\UrlGenerator\\MyEntityGenerator;\nclass MyController extends PageController\n{\n#[UrlGenerator(service: MyEntityGenerator::class, method: 'myActionGenerator', options=[])]\npublic function myAction(MyEntity $entity)\n{\n// do stuff\n}\n}\n
Finally, update config/services.yaml
:
services:\n# ...\nApp\\UrlGenerator\\MyEntityGenerator:\npublic: 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.
.env
into .env.local
.env.local
(documentation). Don't forget to set APP_SECRET
.make build
Run \"$PHP_BIN\" bin/console murph:user:create
and answer questions.
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":"make
Source codes are accessible on Gitnet:
Tasks are scripts executabled from UI. The creation of tasks is based on events.
src/EventSubscriber/MyTaskEventSubscriber.phpnamespace App\\EventSubscriber;\nuse App\\Core\\Event\\Task\\TaskInitEvent;\nuse App\\Core\\Event\\Task\\TaskRunRequestedEvent;\nuse App\\Core\\EventSubscriber\\Task\\TaskEventSubscriber;\nclass MyTaskEventSubscriber extends TaskEventSubscriber\n{\npublic function onInit(TaskInitEvent $event)\n{\n$event->addTask('my_task', 'Example', 'My task');\n}\npublic function onRunRequest(TaskRunRequestedEvent $event)\n{\nif ('my_task' !== $event->getTask()) {\nreturn;\n}\n$event->getOutput()->writeln('My task is started');\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:
_node
and its menu is _menu
_navigation
_locale
_store
_store.navigations
Retrieve a navigation by its code: _store.navigation('the_code')
Retrieve all navigation menus: _navigation.menus
_navigation.menu('the_code')
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)
_store.isActiveNode(node, true)
You can access a page's blocks this way:
{% set myBlock = _page.myBlock.value %}\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":"{{ safe_url(routeName, options, relative) }}
{{ safe_path(routeName, options, relative) }}
{{ node_url(node, options, relative) }}
{{ node_path(node, options, relative) }}
{{ safe_node_url(node, options, relative) }}
{{ 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":"{{ code_url(menuCode, nodeCode, options, relative) }}
{{ code_path(menuCode, nodeCode, options, relative) }}
{{ safe_code_url(menuCode, nodeCode, options, relative) }}
{{ safe_code_path(menuCode, nodeCode, options, relative) }}
When a content could contains tags (eg: '{{url://my_route}}), use
murph_url`. See the example below:
{{ 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.
Added
no-wrap
Fixed
Added
Length
in formsChanged
Fixed
Added
toggle|checkbox_class_when_true
and toggle|checkbox_class_when_false
count
method in repository queryaddForcedFilterHandler
method in repository queryinline_form_validation
option to validate inline forms with custom algoAdded
Fixed
Added
default_value
option in crud fieldsdisplay
option in BooleanFieldFixed
Added
Fixed
Changed
Added
action
in field to add a link to the view page or to the edition pageinline_form
in field to configure to edit the datasetDoubleClick
in the crud configurationAdded
Fixed
Fixed
Fixed
Changed
Added
Fixed
Changed
Added
Fixed
Changed
entity_to_array
Added
Fixed
Added
Changed
Added
Fixed
Changed
Added
make:page
)Fixed
Changed
Added
Changed
Added
Changed
Fixed
Added
Changed
Added
Changed
Added
Changed
core/EventSuscriber
with core/EventSubscriber
Added
Fixed
Added
Fixed
Added
Fixed
Fixed
Changed
Added
Fixed
Changed
Added
Changed
Added
Fixed
Changed
Added
Added
Fixed
Added
Changed
Added
Fixed
Changed
Fixed
Changed
Added
Fixed
Changed
Changed
Fixed
Added
Changed
Changed
Added
Fixed
Changed
Added
Fixed
Changed
Changed
Added
Changed
Fixed
Changed
Changed
Changed
Changed
Added
Fixed
Changed
Added
Changed
Added
Changed
Added
Changed
core/EventSuscriber
with core/EventSubscriber
Added
Fixed
Added
Fixed
Added
Fixed
Fixed
Changed
Added
Fixed
Changed
Added
Changed
Added
Fixed
Changed
Added
Added
Fixed
Added
Changed
Added
Fixed
Changed
Fixed
Murph helps you to manage specific entities with a CRUD manager:
You can configure almost anything:
A generated crud controller contains a method named getConfiguration
. This methods returns a instance of App\\Core\\Crud\\CrudConfiguration
.
setPageTitle(string $page, string $title)
Set the title of the given page.
Example of usage in a CRUD template: <title>{{ configuration.pageTitle('index') }}</title>
.
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>
.
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 %}
.
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 }}
setView(string $context, string $view)
Override a view.
Controller (context) View Descriptionindex
@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/#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 Descriptionproperty
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) {\nreturn $entity->getMyProperty();\n},\n])\n
Example #1$configuration->setField('index', 'My field', TextField::class, [\n'raw' => true,\n'property_builder' => function($entity, array $options) {\nreturn 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$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
view
string
@Core/admin/crud/field/text.html.twig
The templated rendered"},{"location":"crud/configuration/#datefield","title":"DateField","text":"App\\Core\\Crud\\Field\\DateField
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
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
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
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
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 * @ORM\\Column(type=\"integer\", nullable=true)\n */\nprivate $sortOrder;\npublic function getSortOrder(): ?int\n{\nreturn $this->sortOrder;\n}\npublic function setSortOrder(?int $sortOrder): self\n{\n$this->sortOrder = $sortOrder;\nreturn $this;\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.
setBatchAction(string $context, string $action, string $label, callable $callack)
Add a batch action. The callback has 2 arguments:
App\\Core\\Entity\\EntityInterface
App\\Core\\Manager\\EntityManager
use App\\Core\\Entity\\EntityInterface;\nuse App\\Core\\Manager\\EntityManager;\n$configuration->setBatchAction(\n'index',\n'delete',\n'Delete',\nfunction(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:
App\\Core\\Repository\\RepositoryQuery
App\\Core\\Manager\\EntityManager
null
valueDo 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$configuration->setGlobalBatchAction(\n'index',\n'export_json',\n'Export to JSON',\nfunction(RepositoryQuery $query, EntityManager $manager, ?array $selection): JsonResponse {\n$items = $selection ?? $query->find();\nreturn $this->json($items);\n}\n);\n
"},{"location":"crud/generator/","title":"Generator","text":""},{"location":"crud/generator/#prepare-your-entity","title":"Prepare your entity","text":"App\\Core\\Entity\\EntityInterface
(see Entity Manager)php bin/console make:form --help
)The generation is performed in CLI. These information are required:
MyEntityAdminController
)MyEntity
)MyEntityRepositoryQuery
)MyEntityFactory
)MyEntityType
)Simply run php bin/console make:crud-controller
.
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
.
namespace App\\Entity;\nuse App\\Repository\\MyEntityRepository;\nuse Doctrine\\ORM\\Mapping as ORM;\nuse App\\Core\\Entity\\EntityInterface;\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 entitiesApp\\Core\\Manager\\TranslatableEntityManager
used for translatable entitiesnamespace App\\Controller;\nuse App\\Core\\Manager\\EntityManager\nuse App\\Entity\\MyEntity;\nuse Symfony\\Component\\HttpFoundation\\Response;\nclass FooController\n{\npublic function foo(EntityManager $entityManager): Response\n{\n$myEntity = new MyEntity();\n// Creates an entity\n$entityManager->create($myEntity);\n// Updates an entity\n$entityManager->update($myEntity);\n// Deletes an entity\n$entityManager->delete($myEntity);\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;\nuse App\\Core\\Entity\\EntityInterface;\nuse App\\Core\\Event\\EntityManager\\EntityManagerEvent;\nuse App\\Core\\EventSubscriber\\EntityManagerEventSubscriber;\nuse App\\Entity\\MyEntity;\nclass MyEntityEventSubscriber extends EntityManagerEventSubscriber\n{\npublic function supports(EntityInterface $entity): bool\n{\nreturn $entity instanceof MyEntity;\n}\npublic function onCreate(EntityManagerEvent $event)\n{\nif (!$this->supports($event->getEntity())) {\nreturn;\n}\n// ...\n}\npublic function onUpdate(EntityManagerEvent $event)\n{\nif (!$this->supports($event->getEntity())) {\nreturn;\n}\n// ...\n}\npublic function onDelete(EntityManagerEvent $event)\n{\nif (!$this->supports($event->getEntity())) {\nreturn;\n}\n// ...\n}\npublic function onPreCreate(EntityManagerEvent $event)\n{\nif (!$this->supports($event->getEntity())) {\nreturn;\n}\n// ...\n}\npublic function onPreUpdate(EntityManagerEvent $event)\n{\nif (!$this->supports($event->getEntity())) {\nreturn;\n}\n// ...\n}\npublic function onPreDelete(EntityManagerEvent $event)\n{\nif (!$this->supports($event->getEntity())) {\nreturn;\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
.
A factory is basically a service which contain at lease a method named create
.
The generation is performed in CLI. These information are required:
MyEntityFactory
)MyEntity
)Simply run php bin/console make:factory
.
namespace App\\Controller;\nuse App\\Factory\\MyEntityFactory;\nuse Symfony\\Component\\HttpFoundation\\Response;\nclass FooController\n{\npublic function foo(MyEntityFactory $factory): Response\n{\n$entity = $factory->create();\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
.
namespace App\\Entity;\nuse App\\Repository\\MyEntityRepository;\nuse Doctrine\\ORM\\Mapping as ORM;\nuse App\\Core\\Entity\\EntityInterface;\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:
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;\nuse App\\Core\\Repository\\RepositoryQuery;\nuse Knp\\Component\\Pager\\PaginatorInterface;\nclass MyEntityRepositoryQuery extends RepositoryQuery\n{\npublic function __construct(MyEntityRepository $repository, PaginatorInterface $paginator)\n{\nparent::__construct($repository, 'm', $paginator);\n}\n// Example of custom filter\npublic 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);\nreturn $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;\nuse App\\Repository\\MyEntityRepositoryQuery\nuse Symfony\\Component\\HttpFoundation\\Response;\nclass FooController\n{\npublic function foo(MyEntityRepositoryQuery $query): Response\n{\n$entities = $query->create()->find();\n$entity = $query->create()->findOne();\n// Filtered and sorted entities\n$entities = $query->create()\n->orderBy('.id', 'DESC')\n->where('.isPublished = true')\n->find();\n// ...\n}\n}\n
"},{"location":"entities/query/#custom-methods","title":"Custom methods","text":"// ...\nclass MyEntityRepositoryQuery extends RepositoryQuery\n{\n// ...\npublic 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);\nreturn $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:
// ...\nuse App\\Entity\\Something;\nclass MyEntityRepositoryQuery extends RepositoryQuery\n{\n// ...\npublic function filterHandler(string $name, $value): self\n{\nif ($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:
// ...\nclass MyEntityRepositoryQuery extends RepositoryQuery\n{\npublic function __construct(Repository $repository, PaginatorInterface $paginator)\n{\n// ...\n$this->addForcedFilterHandler('foo');\n}\npublic function filterHandler(string $name, $value): self\n{\n// ...\nif ($name === 'foo) {\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;\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;\nclass SettingEventSubscriber extends EventSubscriber\n{\nprotected SettingManager $manager;\npublic function __construct(SettingManager $manager)\n{\n$this->manager = $manager;\n}\npublic 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}\npublic function onFormInit(SettingEvent $event)\n{\n$data = $event->getData();\n$builder = $data['builder'];\n$entity = $data['entity'];\nif (in_array($entity->getCode(), ['app_font_color', 'app_background_color'])) {\n$builder->add(\n'value',\nColorType::class,\n[\n'label' => $entity->getLabel(),\n]\n);\n}\nif (in_array($entity->getCode(), ['app_maintenance_enabled'])) {\n$builder->add(\n'value',\nCheckboxType::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.
namespace App\\Controller;\nuse App\\Core\\Setting\\SettingManager;\nuse Symfony\\Component\\HttpFoundation\\Response;\nclass FooController\n{\npublic 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
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.
namespace App\\Controller;\nuse App\\Core\\Setting\\SettingManager;\nuse Symfony\\Component\\HttpFoundation\\Response;\nclass FooController\n{\npublic 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
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 modalThe 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;\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;\nclass NavigationSettingEventSubscriber extends EventSubscriber\n{\nprotected NavigationSettingManager $manager;\npublic function __construct(NavigationSettingManager $manager)\n{\n$this->manager = $manager;\n}\npublic function onInit(NavigationSettingEvent $event)\n{\n$data = $event->getData();\n$navigation = $data['navigation'];\n$this->manager->init($navigation, 'nav_tracker_code', 'Stats', 'Tracker', '');\n$this->manager->init($navigation, 'nav_contact_email', 'Contact', 'Email', 'foo@example.com');\n}\npublic function onFormInit(NavigationSettingEvent $event)\n{\n$data = $event->getData();\n$builder = $data['builder'];\n$entity = $data['entity'];\nif (in_array($entity->getCode(), ['nav_tracker_code'])) {\n$builder->add(\n'value',\nTextType::class,\n[\n'label' => $entity->getLabel(),\n]\n);\n}\nif (in_array($entity->getCode(), ['nav_contact_email'])) {\n$builder->add(\n'value',\nEmailType::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.
namespace App\\Controller;\nuse App\\Core\\Setting\\NavigationSettingManager;\nuse App\\Core\\Site\\SiteRequest;\nuse Symfony\\Component\\HttpFoundation\\Response;\nclass FooController\n{\npublic 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
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.
namespace App\\Controller;\nuse App\\Core\\Setting\\NavigationSettingManager;\nuse App\\Core\\Site\\SiteRequest;\nuse Symfony\\Component\\HttpFoundation\\Response;\nclass FooController\n{\npublic 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
You can also edit them from UI:
"},{"location":"tree/","title":"Tree manager","text":"Murph manages contents this way:
%%{\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.
label
is the label displayed whenever necessary (eg: Top menu
)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.
label
is the label displayed whenever necessary (eg: Example
)locale
is the language used in the content (eg: en
)code
is a unique technical identifier useful in templating, routing and settings (eg: example_en
)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 wantIf 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:
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.
The routing tab is very important. It allows you to define all parameters related to:
code
To add a controller in the list, edit config/packages/app.yaml
:
core:\nsite:\ncontrollers:\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:\nsite:\nsecurity:\nroles:\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.
namespace App\\Entity\\Page;\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#[ORM\\Entity]\nclass YourPage extends Page\n{\npublic function buildForm(FormBuilderInterface $builder, array $options)\n{\n$builder->add(\n'myBlock',\nTextBlockType::class,\n[\n'label' => 'My block',\n'options' => [\n// options given to the sub form\n],\n]\n);\n// ...\n}\npublic function setMyBlock(Block $block)\n{\nreturn $this->setBlock($block);\n}\npublic function getMyBlock(): Block\n{\nreturn $this->getBlock('myBlock');\n}\n// ...\n}\n
Then edit config/packages/app.yaml
and add your page:
core:\nsite:\npages:\nApp\\Entity\\Page\\SimplePage:\nname: 'Simple page'\ntemplates:\n- {name: \"Default\", file: \"page/simple/default.html.twig\"}\nApp\\Entity\\Page\\YourPage:\nname: 'Your page'\ntemplates:\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
.
App\\Core\\Form\\Site\\Page\\TextareaBlockType
will render a symfony TextareaType
.
App\\Core\\Form\\Site\\Page\\ChoiceBlockType
will render a symfony ChoiceType
.
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;\npublic function getMyBlock(): Block\n{\nreturn $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.
App\\Core\\Form\\Site\\Page\\EditorJsTextareaBlockType
will render a EditorJs widget.
App\\Core\\Form\\Site\\Page\\GrapesJsBlockType
will render a GrapesJS editor.
App\\Core\\Form\\Site\\Page\\TinymceTextareaBlockType
will render a Tinymce editor.
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;\npublic function getMyBlock(): Block\n{\nreturn $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;\npublic function getMyBlock(): Block\n{\nreturn $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;\nuse App\\Core\\Event\\Page\\PageEditEvent;\nuse App\\Entity\\Page\\YourPage;\nclass PageEventSubscriber implements EventSubscriberInterface\n{\npublic static function getSubscribedEvents()\n{\nreturn [\nPageEditEvent::FORM_INIT_EVENT => ['onFormInit'],\n];\n}\npublic function onFormInit(PageEditEvent $event)\n{\nif ($event->getPage() instanceof YourPage) {\n$event->addPageBuilderOptions([\n// options\n]);\n}\n}\n}\n
"},{"location":"utils/cache/","title":"Cache Manager","text":"App\\Core\\Cache\\SymfonyCacheManager
cleanRouting()
: clear the cache of routescleanAll(OutputInterface $output = null)
: clean and warmup all cacheApp\\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.
namespace App/Entity;\nuse use App\\Core\\Entity\\EntityInterface;\nuse Doctrine\\ORM\\Mapping as ORM;\nuse App\\Core\\Doctrine\\Timestampable;\nuse App\\Core\\Entity\\EntityInterface;\nuse App\\Repository\\FooRepository;\nuse Doctrine\\ORM\\Mapping as ORM;\n#[ORM\\Entity(repositoryClass: FooRepository::class)]\n#[ORM\\HasLifecycleCallbacks]\nclass FooEntity implements EntityInterface\n{\nuse Timestampable;\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$path = 'path/to/file';\n$file = FileAttribute::handleFile($path); // returns an instance of File\n$path = 'path/to/file';\n$file = FileAttribute::handleFile($path, Bar::class); // returns an instance of Bar\n$path = 'path/to/removed_file';\n$file = FileAttribute::handleFile($path); // returns null\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;\npublic function upload(Request $request, FileUploadHandler $fileUpload)\n{\n$entity = new Foo();\n$form = $this->createForm(FooType::class, $foo);\nif ($request->isMethod('POST')) {\n$form->handleRequest($request);\nif ($form->isValid()) {\n$fileDirectory = 'uploads/';\n$keepOriginalFilename = false;\n$fileUpload->handleForm(\nuploadedFile: $form->get('image')->getData(), // Symfony\\Component\\HttpFoundation\\File\\UploadedFile or null\npath: $fileDirectory,\n// optional\nafterUploadCallback: function ($filename) use ($entity, $fileDirectory) {\n$entity->setImage($fileDirectory.$filename);\n},\n// optional\nkeepOriginalFilename: $keepOriginalFilename\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;\npublic function foo(MailNotifier $notifier, UserRepositoryQuery $query)\n{\n// ...\n$notifier\n->init()\n->setSubject('Your bill')\n->addRecipient('john.doe@example.com')\n->addRecipients(array_map(\nfn($u) => $u->getEmail(),\n$query->create()->where('.isAdmin = true')->find()\n), 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.
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;\nuse App\\Core\\Form\\Type\\EditorJsTextareaType;\nuse Symfony\\Component\\Form\\AbstractType;\nuse Symfony\\Component\\Form\\FormBuilderInterface;\nclass ExampleType extends AbstractType\n{\npublic function buildForm(FormBuilderInterface $builder, array $options)\n{\n$builder->add(\n'myField',\nEditorJsTextareaType::class\n);\n// ...\n}\n// ...\n}\n
Modified data should return stringified JSON array if empty:
public function getMyField(): string\n{\nif (empty($this->myField)) {\n$this->myField = '[]';\n}\nreturn $this->myField;\n}\n
"},{"location":"utils/editors/editorjs/#page-form","title":"Page form","text":"// src/Entity/Page/YourPage.php\nnamespace App\\Entity\\Page;\nuse App\\Core\\Entity\\Site\\Page\\Block;\nuse App\\Core\\Form\\Site\\Page\\EditorJsTextareaBlockType;\n#[@ORM\\Entity]\nclass YourPage extends Page\n{\npublic function buildForm(FormBuilderInterface $builder, array $options)\n{\n$builder->add(\n'myBlock',\nEditorJsTextareaBlockType::class,\n[\n'label' => 'My block',\n'row_attr' => [\n],\n'options' => [\n// options given to the sub form\n],\n]\n);\n// ...\n}\npublic function setMyBlock(Block $block)\n{\nreturn $this->setBlock($block);\n}\npublic function getMyBlock(): Block\n{\nreturn $this->getBlock('myBlock');\n}\n// ...\n}\n
"},{"location":"utils/editors/editorjs/#rendering","title":"Rendering","text":"Editor.js will generate a JSON which contains blocks.
Supported blocks:
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:\neditor_js:\nblocks:\nparagraph: 'path/to/paragraph.html.twig'\nheader: '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;\nuse App\\Core\\Form\\Type\\GrapesJsType;\nuse Symfony\\Component\\Form\\AbstractType;\nuse Symfony\\Component\\Form\\FormBuilderInterface;\nclass ExampleType extends AbstractType\n{\npublic function buildForm(FormBuilderInterface $builder, array $options)\n{\n$builder->add(\n'myField',\nGrapesJsType::class\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;\nuse App\\Core\\Entity\\Site\\Page\\Block;\nuse App\\Core\\Form\\Site\\Page\\GrapesJsBlockType;\n#[ORM\\Entity]\nclass YourPage extends Page\n{\npublic function buildForm(FormBuilderInterface $builder, array $options)\n{\n$builder->add(\n'myBlock',\nGrapesJsBlockType::class,\n[\n'label' => 'My block',\n'row_attr' => [\n],\n'options' => [\n// options given to the sub form\n],\n]\n);\n// ...\n}\npublic function setMyBlock(Block $block)\n{\nreturn $this->setBlock($block);\n}\npublic function getMyBlock(): Block\n{\nreturn $this->getBlock('myBlock');\n}\n// ...\n}\n
"},{"location":"utils/editors/grapesjs/#options","title":"Options","text":"There are 3 modes:
bootstrap4
presetWebpage
presetNewsletter
To specify a mode, you must define the attribute data-grapesjs
:
$builder->add(\n'myField',\nGrapesJsType::class,\n[\n// ...\n'attr' => [\n'data-grapesjs' => 'bootstrap4',\n],\n]\n);\n```php-inline title=\"src/Entity/Page/YourPage.php\"\n$builder->add(\n'myBlock',\nGrapesJsBlockType::class,\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.
{% set html = value|grapesjs_html %}
{% set css = value|grapesjs_css %}
Depending of the mode, you will need to import in the app sass file:
@import \"~bootstrap/scss/bootstrap.scss\";
@import \"~grapesjs-preset-webpage/dist/grapesjs-preset-webpage.min.css\";
@import \"~grapesjs-preset-newsletter/dist/grapesjs-preset-newsletter.css\";
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;\nuse App\\Core\\Form\\Type\\TinymceTextareaType;\nuse Symfony\\Component\\Form\\AbstractType;\nuse Symfony\\Component\\Form\\FormBuilderInterface;\nclass ExampleType extends AbstractType\n{\npublic function buildForm(FormBuilderInterface $builder, array $options)\n{\n$builder->add(\n'myField',\nTinymceTextareaType::class\n);\n// ...\n}\n// ...\n}\n
"},{"location":"utils/editors/tinymce/#page-form","title":"Page form","text":"src/Entity/Page/YourPage.phpnamespace App\\Entity\\Page;\nuse App\\Core\\Entity\\Site\\Page\\Block;\nuse App\\Core\\Form\\Site\\Page\\TinymceTextareaBlockType;\n#[ORM\\Entity]\nclass YourPage extends Page\n{\npublic function buildForm(FormBuilderInterface $builder, array $options)\n{\n$builder->add(\n'myBlock',\nTinymceTextareaBlockType::class,\n[\n'label' => 'My block',\n'row_attr' => [\n],\n'options' => [\n// options given to the sub form\n],\n]\n);\n// ...\n}\npublic function setMyBlock(Block $block)\n{\nreturn $this->setBlock($block);\n}\npublic function getMyBlock(): Block\n{\nreturn $this->getBlock('myBlock');\n}\n// ...\n}\n
"},{"location":"utils/editors/tinymce/#options","title":"Options","text":"There are 2 predefined modes:
default
light
To specify a mode, you must define the attribute data-tinymce
:
$builder->add(\n'myField',\nTinymceTextareaType::class,\n[\n// ...\n'attr' => [\n'data-tinymce' => 'light',\n],\n]\n);\n
src/Entity/Page/YourPage.php$builder->add(\n'myBlock',\nTinymceTextareaBlockType::class,\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'\nwindow.tinymce.language = 'fr_FR'\nwindow.tinymce.murph.modes.myCustomMode = {\nplugins: '...',\nmenubar: '...',\ntoolbar: '...'\nquickbars_selection_toolbar: '...'\ncontextmenu: '...'\ntemplates: [\n{\ntitle: 'Container',\ndescription: 'Add a bootstrap container',\ncontent: '<div class=\"container\"><div class=\"selcontent\"></div></div>'\n}\n// ...\n],\ncontent_style: '...'\n}\n
src/Form/ExampleType.php$builder->add(\n'myField',\nTinymceTextareaType::class,\n[\n// ...\n'attr' => [\n'data-tinymce' => 'myCustomMode',\n],\n]\n);\n
src/Entity/Page/YourPage.php$builder->add(\n'myBlock',\nTinymceTextareaBlockType::class,\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 }}
.
When you need to manage a collection in a form, you can use App\\Core\\Form\\Type\\CollectionType
with these options:
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.