# Developer Documentation > Developer documentation for Cohesivo — architecture, APIs, templating, and extensibility for building and customizing Ibexa DXP projects. # Ibexa Developer Documentation # Ibexa Developer Documentation > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). ## How to start? [Follow the Beginner tutorial](https://doc.ibexa.co/en/saas/tutorials/beginner_tutorial/beginner_tutorial/index.md) [Explore the APIs](https://doc.ibexa.co/en/saas/api/api/index.md) [Go through the First steps](https://doc.ibexa.co/en/saas/getting_started/first_steps/index.md) ## The latest Cohesivo is v5.0 LTS The latest v5.0 LTS release is 5.0.10. You can now update your application. [Release notes](https://doc.ibexa.co/en/saas/release_notes/index.md) ## The newest LTS Update is Translations management Use machine translation, side-by-side editing view, and a review cycle to improve your translation experience. [Learn more about this LTS Update](https://doc.ibexa.co/en/saas/multisite/translations_management/translations_management/index.md) [Discover other LTS Updates](https://doc.ibexa.co/en/saas/ibexa_products/editions/#lts-updates) ## Notable changes in v5.0 - [AI Actions](https://doc.ibexa.co/en/saas/release_notes/#ai-actions) - [Date and time attribute for product catalog](https://doc.ibexa.co/en/saas/release_notes/#date-and-time-attribute) - [Symbol attribute for product catalog](https://doc.ibexa.co/en/saas/release_notes/#symbol-attribute) ## Most popular pages - [PHP API](https://doc.ibexa.co/en/saas/api/php_api/php_api/index.md) - [RichText and Online Editor](https://doc.ibexa.co/en/saas/content_management/rich_text/rich_text/index.md) - [Search API](https://doc.ibexa.co/en/saas/search/search_api/index.md) - [Content model](https://doc.ibexa.co/en/saas/content_management/content_model/index.md) - [Images](https://doc.ibexa.co/en/saas/content_management/images/images/index.md) - [Page blocks](https://doc.ibexa.co/en/saas/content_management/pages/page_blocks/index.md) ## Manage your Cohesivo ### [Content](https://doc.ibexa.co/en/saas/content_management/content_management/index.md) - [Content model](https://doc.ibexa.co/en/saas/content_management/content_model/index.md) - [File management](https://doc.ibexa.co/en/saas/content_management/file_management/file_management/index.md) - [Pages](https://doc.ibexa.co/en/saas/content_management/pages/pages/index.md) ### [Product catalog](https://doc.ibexa.co/en/saas/product_catalog/product_catalog/index.md) - [Product catalog configuration](https://doc.ibexa.co/en/saas/product_catalog/product_catalog_configuration/index.md) - [Quable Integration](https://doc.ibexa.co/en/saas/product_catalog/quable/quable/index.md) - [Catalogs](https://doc.ibexa.co/en/saas/product_catalog/catalogs/index.md) - [Prices](https://doc.ibexa.co/en/saas/product_catalog/prices/index.md) ### [Customer](https://doc.ibexa.co/en/saas/customer_management/customer_portal/index.md) - [Configuration](https://doc.ibexa.co/en/saas/customer_management/cp_configuration/index.md) - [Build Customer Portal](https://doc.ibexa.co/en/saas/customer_management/cp_page_builder/index.md) - [Registration form](https://doc.ibexa.co/en/saas/customer_management/create_user_registration_form/index.md) # Cohesivo editions # Cohesivo editions > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). Learn more about various Cohesivo editions' features to help yourself choose the right one for your project. Three Cohesivo product editions are available to help you accelerate your digital transformation at the speed and cost that work best for you. - [Ibexa Headless edition product guide](https://ez-systems-developer-documentation--3381.com.readthedocs.build/en/3381/ibexa_products/ibexa_headless/): Get to know Ibexa Headless - an edition that focuses on content management. - [Ibexa Experience edition product guide](https://ez-systems-developer-documentation--3381.com.readthedocs.build/en/3381/ibexa_products/ibexa_experience/): Learn about all the main attributes, features, and benefits of the customer-focused Ibexa Experience edition. ## Feature comparison Compare all features available in Ibexa Headless, Ibexa Experience, and Ibexa Commerce to help you choose the right products for your needs: | Feature | Ibexa Headless | Ibexa Experience | Ibexa Commerce | | ------------------------------------------------------------------------------------------------------------------------------------------- | -------------- | ---------------- | -------------- | | [Content model](https://doc.ibexa.co/en/saas/content_management/content_model/index.md) | Yes | Yes | Yes | | [AI Actions](https://doc.ibexa.co/en/saas/ai/ai_actions/ai_actions_guide/index.md) | Yes | Yes | Yes | | [User management](https://doc.ibexa.co/en/saas/users/user_management_guide/index.md) | Yes | Yes | Yes | | [Focus Mode](https://doc.ibexa.co/projects/userguide/en/6.0/getting_started/discover_ui/#focus-mode) | Yes | Yes | Yes | | [Image editor](https://doc.ibexa.co/projects/userguide/en/6.0/image_management/edit_images/) | Yes | Yes | Yes | | [Content scheduler](https://doc.ibexa.co/projects/userguide/en/6.0/content_management/schedule_publishing/) | Yes | Yes | Yes | | [SEO](https://doc.ibexa.co/projects/userguide/en/6.0/search_engine_optimization/seo/) | Yes | Yes | Yes | | [Content translation](https://doc.ibexa.co/projects/userguide/en/6.0/content_management/translate_content/) | Yes | Yes | Yes | | [Search](https://doc.ibexa.co/projects/userguide/en/6.0/search/search_for_content/) | Yes | Yes | Yes | | [Editorial workflow](https://doc.ibexa.co/projects/userguide/en/6.0/content_management/workflow_management/editorial_workflow/) | Yes | Yes | Yes | | [Digital Asset Management](https://doc.ibexa.co/projects/userguide/en/6.0/dam/ibexa_dam/) | Yes | Yes | Yes | | [Product catalog capabilities](https://doc.ibexa.co/projects/userguide/en/6.0/product_catalog/product_catalog/) | Yes | Yes | Yes | | [Date and time attribute type](https://doc.ibexa.co/en/saas/product_catalog/attributes/date_and_time/index.md) | Yes | Yes | Yes | | [Symbol attribute type](https://doc.ibexa.co/en/saas/product_catalog/attributes/symbol_attribute_type/index.md) | Yes | Yes | Yes | | [Recommendations](https://doc.ibexa.co/en/saas/recommendations/raptor_integration/raptor_connector_guide/index.md) | Yes | Yes | Yes | | [Migrations](https://doc.ibexa.co/en/saas/content_management/data_migration/managing_migrations/index.md) | Yes | Yes | Yes | | [Ibexa Connect](https://doc.ibexa.co/projects/connect/en/latest/) | Yes | Yes | Yes | | [OAuth client](https://doc.ibexa.co/en/saas/users/oauth_client/index.md) | Yes | Yes | Yes | | [OAuth Server](https://doc.ibexa.co/en/saas/users/oauth_server/index.md) | Yes | Yes | Yes | | [Site Factory](https://doc.ibexa.co/en/saas/multisite/site_factory/site_factory/index.md) | | Yes | Yes | | [Customizable Dashboard](https://doc.ibexa.co/projects/userguide/en/6.0/getting_started/dashboard/work_with_dashboard/#customize-dashboard) | | Yes | Yes | | [Page Builder](https://doc.ibexa.co/en/saas/content_management/pages/page_builder_guide/index.md) | | Yes | Yes | | [Form Builder](https://doc.ibexa.co/en/saas/content_management/forms/form_builder_guide/index.md) | | Yes | Yes | | [Scheduler tab](https://doc.ibexa.co/projects/userguide/en/6.0/content_management/schedule_publishing/#scheduler-tab) | | Yes | Yes | | [Content Scheduler block](https://doc.ibexa.co/projects/userguide/en/6.0/content_management/schedule_publishing/#content-scheduler-block) | | Yes | Yes | | [Corporate account management](https://doc.ibexa.co/projects/userguide/en/6.0/customer_management/manage_customers/) | | Yes | Yes | | [Customer Portal](https://doc.ibexa.co/en/saas/customer_management/customer_portal_guide/index.md) | | Yes | Yes | | [Segments](https://doc.ibexa.co/en/saas/administration/admin_panel/segments_admin_panel/index.md) | | Yes | Yes | | [Recent activity](https://doc.ibexa.co/en/saas/administration/recent_activity/recent_activity/index.md) | | Yes | Yes | | [Qualifio add-on](https://doc.ibexa.co/projects/userguide/en/6.0/qualifio/qualifio/) | | Yes | Yes | | [Raptor CDP (Customer Data Platform) add-on](https://doc.ibexa.co/en/saas/raptor_cdp/raptor_cdp_guide/index.md) | | Yes | Yes | ## LTS Updates LTS Updates are opt-in packages that bring additional features to the LTS releases that they enhance. The features brought by LTS Updates become standard parts of the next LTS release. | Feature | Ibexa Headless | Ibexa Experience | Ibexa Commerce | | -------------------------------------------------------------------------------------------------------------------------------- | -------------- | ---------------- | -------------- | | [Anthropic connector](https://doc.ibexa.co/en/saas/ai/ai_actions/configure_ai_actions/#install-anthropic-connector) | Yes | Yes | Yes | | [Google Gemini connector](https://doc.ibexa.co/en/saas/ai/ai_actions/configure_ai_actions/#install-google-gemini-connector) | Yes | Yes | Yes | | [Integrated help](https://doc.ibexa.co/en/saas/administration/back_office/integrated_help/index.md) | Yes | Yes | Yes | | [MCP servers](https://doc.ibexa.co/en/saas/ai/mcp/mcp_guide/index.md) | Yes | Yes | Yes | | [Translations management](https://doc.ibexa.co/en/saas/multisite/translations_management/translations_management_guide/index.md) | Yes | Yes | Yes | # Ibexa Headless edition product guide > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). Get to know Ibexa Headless - an edition that focuses on content management. ## What is Ibexa Headless The Headless edition of Cohesivo focuses on content management. It provides tools to collaboratively create content, and interfaces (API) to distribute this content. Multilingual, multichannel, extensible, Ibexa Headless is an advanced Content Management Framework (CMF) with product catalog capabilities, and a Digital Asset Management (DAM) repository. It's provided without a default front office, but with a complete back office and several APIs to manage and access content. ![Ibexa Headless](https://doc.ibexa.co/en/saas/ibexa_products/img/ibexa_headless.png) ## Availability To start using Ibexa Headless you must purchase a product license. For more information, see [Ibexa Headless license pricing](https://www.ibexa.co/products/pricing?tab=1). You can [contact us](https://www.ibexa.co/about-ibexa/contact-us) or [contact one of our partners](https://www.ibexa.co/partners). ## How it works ### Editorial stage You access with any web browser from any platform to a rich back office, the main place to - define users and their rights (for example, customers, subscribers, or editors), - organize content (content types, fields, tree, tags, languages, and more), - edit content in a collaborative workplace with versions and workflows. Then, content is available to end users through REST, GraphQL, or every output you can imagine like websites or apps. ### Technical backstage When you have a license, you install Ibexa Headless through Composer on an architecture including at least a web server with PHP and a relational database server. For performance, several bricks can be added to your stack such as a reverse proxy or a search engine. Ibexa Headless is based on Symfony. Any Symfony developer, or even PHP developer, can quickly learn how to extend it with the help of an online documentation. By using a version control system and environment variables, you can deploy your configuration and extensions on several environments including Ibexa Cloud. Standard web APIs and [Ibexa Connect](https://doc.ibexa.co/projects/connect/en/latest/general/ibexa_connect/) help establish interoperability, even if you aren't an advanced developer. ![Ibexa Headless data inputs and outputs](https://doc.ibexa.co/en/saas/ibexa_products/img/headless.png) APIs summary: - The REST and GraphQL APIs give access to the content in standardized ways. - The OAuth 2 [Client](https://doc.ibexa.co/en/saas/users/oauth_client/index.md) and [Server](https://doc.ibexa.co/en/saas/users/oauth_server/index.md) allow to connect to an SSO or be the SSO. - The design engine and its theme templates mechanism allows to serve the content in several shapes. - The PHP API opens Ibexa Headless to extendability to fit your needs. For example, content can be computed, edited, or served in specific ways such as scheduled/live imports/exports, automated edition tasks, or specific controllers to communicate with other applications. ## Capabilities and benefits Ibexa Headless is a tool box with a back office. It comes without a default front office. You don't lose time to develop a theme for a provided front office before discovering it doesn't fit your needs. No distraction. Ibexa Headless helps you focus on the content, create and organize with its straightforward user interface (UI), imagine its inputs/outputs, and implement them with its various layers' APIs. ### Core features The core of Ibexa Headless offers everything to structure your content repositories and access them. #### Content model Content modeling and management are the foundation of Cohesivo with the following main layers: - Content items are organized as a tree in a repository. - An item can have multiple locations in this tree. - Content items are typed. - Content types are sets of typed data fields, with optional conditions on the possible values. - Rich Text field type comes with an [online editor](https://doc.ibexa.co/en/saas/content_management/rich_text/online_editor_guide/index.md). - Multilingual, it can store a content in several languages, the content model defines which field must be translated, and which don't vary. For more information, see [Content management product guide](https://doc.ibexa.co/en/saas/content_management/content_management_guide/index.md). #### User management User and user group rights are set by roles with thin granular limited permission policies in a safe deny-by-default security system. Users are content items as well, so your knowledge about content management is reused. For more information, see [User management product guide](https://doc.ibexa.co/en/saas/users/user_management_guide/index.md). #### Content access There are many paths to access the content in many shapes: - The REST API and GraphQL API support access to, and edition of the content. - Ibexa Headless offers a complete PHP API to extend the ways to access content. - A design engine and a view controller offer to create plain text content views (such as HTML, JSON, XML, CSS, JS, CSV, or Markdown), and to factorize those views by using theme cascades. This design engine is used in the back office which is equally extendable. - Multichannel, content can be accessed through several channel configurations, such as the domain name it replies to, the sub-part of the content tree it starts from, the users rights, or the design theme. The back office itself is such a channel. - Multi-repository, the same platform can use separate databases if data isolation is needed between channel groups. ### Advanced features On top of this strong core, Ibexa Headless brings tools to increase user experience, from final front users to back office contributors. #### Complete platform Ibexa Headless is a complete platform, which comes with the following components to enhance user's journey: - [Recommendations](https://doc.ibexa.co/en/saas/recommendations/raptor_integration/raptor_connector_guide/index.md) connector, which allows you to recommend content to end users according to their behavior, or, when authenticated, by matching with their segment/group. - Content scheduler, which allows you to establish the future of the content and use events to have a living front application, even when the editorial team is absent or reduced. This way, visitors can discover new content at midnight, during weekends or vacations. A calendar summarises those scheduled content events. Like everything in the back office, the calendar is extendable: you can add an event source to coordinate content events with other company events. #### Many ways to structure and organize content [Product catalog](https://doc.ibexa.co/en/saas/product_catalog/product_catalog_guide/index.md) helps organize complex products and their catalogs: - Products are organized by using product types, variants, catalogs, categories, and tags. - Product attributes are grouped and factorized among product types. For example, fabric + color + size can be shared by many clothing product types. - Product variants can rapidly be created by the automatic declination of attributes that have a defined set of values. - With taxonomy, you can tag content items to organize them by topics in a much intuitive way for the editor than a content tree with multiple locations would. Tags themselves are organized in a tree, and synonyms are linked to favorite terms. Tag organization can be handled by a supervisor who doesn't need to move content items around a corporate content tree. At search time, tags can be keywords with a high value in relevance score to help the end user having results closer to the searched topic. #### Collaboration Several features help end users collaborate on the content, such as: - Version comparison helps track changes and solve concurrent editing conflicts. - Workflows helps with collaborative editing chain. A built-in “Quick review“ workflow allows an editor to send a content draft to a colleague for review, and comment or publishing. But, as a framework, more complex workflows can be imagined, with several steps and paths, even some automated tasks. #### Accelerated content editing - Ibexa Headless's content tree has several actions available directly on its items. For example, no need to open a content to hide it, you can do it directly from the content tree. - An Image Editor offers to crop and flip images. When serving the image in various context, you can even set a focal point to indicate to automated cropping which part of the image should be kept. - A Digital Asset Management (DAM) helps you crawl through your image resources to use and reuse them in your content. And a DAM connector allows you to search for images hosted on third party DAM servers. - [AI Actions](https://doc.ibexa.co/en/saas/ai/ai_actions/ai_actions_guide/index.md) help you automate time-consuming editorial tasks. #### Network integration ##### Intranets and extranets - Ibexa Connect's role is to create application interconnections with low code and drag-and-drop, in a compelling visual interface. Complex data flows can be easily implemented with a huge library of connectors and actions for famous to specific applications. For more information, see [Ibexa Connect product guide](https://doc.ibexa.co/projects/connect/en/latest/general/ibexa_connect/). - An OAuth 2 server offers the possibility to use the platform as the authentication service for other applications. - An OAuth 2 client supports authentication with a third-party OAuth 2 server. - A DAM Connector, previously mentioned, helps to access any image repository when needing to illustrate a content. - Ibexa Headless supports Elasticsearch and Solr. It gives the choice between using Solr or Elasticsearch as a search engine, whether hosted on Ibexa Cloud or on-premises. This choice might be influenced by technology you already use, or you want to invest in for other internal projects. - Ibexa Headless offers to export and import from command line part of the content model or content items. For example, it can be used to move new content types and items from a staging instance to the production one. ##### Internet, delivery, web search engines, and social networks - Ibexa Headless comes with the support of Fastly content delivery network (CDN). The HTTP cache varies on current user's role and is purged when content changes. With its huge network of points of presence (POP) around the world, Fastly is quickly delivering cached content from nearest server for a better user experience. - A Search Engine Optimization (SEO) field implements best practices about web search engine indexing and social network sharing. It covers canonical URLs which are mandatory if multiple locations are used for a same content item to avoid duplicate content, Open Graph protocol to better describe a content item to social networks and search engine, and Twitter Cards. ## Use cases As a content repository with an omnipotent back office, many APIs to absorb, compute and distribute content, even a recommendation engine to deliver the right content to various readers, Ibexa Headless can be used in several cases. Here are few examples. ### Brick and mortar, but with an online showcase If you prefer the human warmth of a retail store, if your products' numerous complex options should be discussed, or if you're not ready yet to sell online, Ibexa Headless helps to build an exposition of your product catalog and your philosophy, an online presence to keep earlier customers interested and gather new ones. It can be a structuring first step to test customer's adoption of your website, before increasing user experience with Ibexa Experience, and finally becoming an online store with Ibexa Commerce. ### Large network with multiple inputs and outputs Departments, subsidiaries, and even partners now produce content in the same repository from the same collaborative workspace. Thanks to migration feature and PHP API, existing content has been imported from previous repositories. Fine-tuned user rights and workflows ensure that each collaborator can focus on their own tasks without the risk to disturb the content model or content organization. Content is distributed on several websites and applications, some running on the Ibexa platform itself, some on third parties' servers, some as native mobile apps. Part of the content has multiple locations or is translated, and reused from place to place. While the back office offers to search into the whole repository, the front end apps have correctly circumscribed search capabilities. # Ibexa Experience edition product guide > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). Learn about all the main attributes, features, and benefits of the customer-focused Ibexa Experience edition. ## What is Ibexa Experience Ibexa Experience is a Cohesivo edition that focuses on the customer. It offers smooth consumer journey and great online experience. In everything you do, it places your clients first. With Experience edition you can empower Editors to quickly create new pages or personalized content, and improve their daily work. It also provides tools for using segmentation and targeting, and it can be widely used in B2B thanks its features and integrations. ![Ibexa Experience](https://doc.ibexa.co/en/saas/ibexa_products/img/ibexa_experience.png) ## Availability To start using Ibexa Experience, you need to purchase a product license. For more information, see [Ibexa Experience license pricing](https://www.ibexa.co/products/pricing?tab=2). You can also [contact us](https://www.ibexa.co/about-ibexa/contact-us) or [one of our partners](https://www.ibexa.co/partners). ## How it works ### Technical backstage Ibexa Experience is based on [Symfony](https://symfony.com/doc/7.4). With a help of documentation and trainings, any developer familiar with Symfony or simply PHP may learn how to use available extension points and extend the platform. Ibexa Experience is built on top of [Ibexa Headless](https://doc.ibexa.co/en/saas/ibexa_products/ibexa_headless/index.md), therefore it includes all bundles, APIs, and [features that come with Headless edition](https://doc.ibexa.co/en/saas/ibexa_products/ibexa_headless/#core-features), but also more advanced features for digital experience management. Version control systems and environment variables allow you to deploy your projects and settings on several environments, such as Ibexa Cloud. ## Capabilities and benefits With Ibexa Experience you can focus on your customers and treat each one as a VIP. It has everything that you may need to offer a transformative digital experience, from developing new websites or portals, through eye-catching landing pages and personalized product suggestions, to managing SEO strategies across several locations. ### Core features Ibexa Experience comes with a variety of new features designed to help you create an exceptional customer experience. #### Page Builder Ibexa Experience brings the [Page Builder](https://doc.ibexa.co/en/saas/content_management/pages/page_builder_guide/index.md), a powerful visual tool that helps you design and modify pages, without requiring advanced technical skills. With its intuitive and user-friendly interface, you can develop pages, tailor content, and create perfectly targeted landing pages. You build pages from ready-to-use elements called blocks, which can be easily configured and customized to suit your needs. Before you start building a page, you also need to select a layout. It has a significant impact on how the content pieces in the drop zones are arranged. ![Page Builder](https://doc.ibexa.co/en/saas/ibexa_products/img/page_builder.png) #### Form Builder [Form Builder](https://doc.ibexa.co/en/saas/content_management/forms/form_builder_guide/index.md) is an intuitive tool that allows you to transform user engagement on your website. With this tool, you can design, deploy, and manage online forms quickly. You can create a variety of forms that consist of different fields, including sign-up forms, surveys, or questionnaires. Additionally, you can monitor and manage the information obtained from website visitors and adjust your forms if needed. ![Form Builder](https://doc.ibexa.co/en/saas/ibexa_products/img/form_builder.png) #### Site Factory [Site Factory](https://doc.ibexa.co/en/saas/multisite/site_factory/site_factory/index.md) is a site management interface, integrated with the back office. It enables you to configure new sites without leaving the administration interface and editing SiteAccess configuration. With this feature you can create and deploy multiple websites at lightning speed and at scale. It allows you to manage expenses and resources while industrializing your web presence. Additionally, together with localized information and tailored product catalogs and prices, it helps you to quickly enter new markets. #### Customizable dashboard Starting from Experience edition of Cohesivo you can [customize the dashboard](https://doc.ibexa.co/projects/userguide/en/6.0/getting_started/dashboard/work_with_dashboard/#customize-dashboard), and you do it with the Dashboard Builder. You can tailor dashboard to your specific needs by choosing from a set of widgets. You can easily preview the sections that you use more often and omit the less significant ones. ![Customizable dashboard](https://doc.ibexa.co/en/saas/ibexa_products/img/customizable_dashboard.png) #### Publish Later You can take complete control of where and when your content blocks are visible to your predefined audiences and [schedule content publication](https://doc.ibexa.co/projects/userguide/en/6.0/content_management/schedule_publishing/). Ibexa Experience comes with a Publish Later feature that allows you to schedule personalized content and reach different user groups at optimal dates and times to boost performance. What's more, you can turn specific content pages and blocks on and off to meet the needs of your marketing campaigns and promotions. Publish Later feature combined with Page Builder allows you to see all changes that you plan for the future. To do it, just use the slider to see all the upcoming changes. #### Customer Portal Use the [Customer Portal](https://doc.ibexa.co/en/saas/customer_management/customer_portal/index.md) and customer management capabilities that come with it, to establish new corporate accounts, manage existing ones, and communicate with your partners within a personalized space. With the help of this feature, you can create customized areas that give users a smooth, integrated experience and provide them with access to a variety of resources, apps, and services from a single point of entry. Using this tool, your customers can change their organization details, invite and see members, self-register, and more. #### Segments [Segmentation](https://doc.ibexa.co/en/saas/administration/admin_panel/segments_admin_panel/index.md) allows you to split up the user base. By assigning users to segments, you can display specific content to selected visitors and tailor the content that they can see. One of the tools that you can use right out of the box is the Targeting block that is available in the Page Builder. Segmentation is also useful with the [Recommendations](https://doc.ibexa.co/en/saas/recommendations/raptor_integration/raptor_connector_guide/index.md). You can assign users to different recommendation groups and create advanced logic with operators to provide your audience with the best recommendations. ![Segments](https://doc.ibexa.co/en/saas/ibexa_products/img/segments.png) #### Raptor CDP (Customer Data Platform) [Raptor CDP](https://doc.ibexa.co/en/saas/raptor_cdp/raptor_cdp_guide/index.md) is an add-on available for the Experience edition of Cohesivo. To use it, you must make arrangements with Ibexa to define the initial configuration. Once you activate Raptor CDP, you can create complete customer profiles, including their interactions, behavior, and preferences. It helps you improve user engagement, conversion rates, and return on investment by segmenting your audience and delivering tailored campaigns and experiences. Additionally, you can manage and analyze campaigns, evaluate customer data, and identify the best ways to improve performance. By using Raptor CDP you can store and manage large volumes of customer data in a structured manner. This central data storage supports business growth with a scalable infrastructure, helping to futureproof your business. ![CDP](https://doc.ibexa.co/en/saas/raptor_cdp/img/cdp.png) #### Qualifio Another add-on available for the Experience edition is [Qualifio](https://doc.ibexa.co/en/saas/qualifio/qualifio/index.md). To use it, you must make arrangements with Ibexa to define the initial configuration, and then get and set up a user account. Qualifio is a data collection tool. It gives you the ability to use the [Qualifio](https://qualifio.com/) tools to engage your audiences. You can use Qualifio's existing templates and interactive elements, such as quizzes, pools, and forms, to create visually appealing, customized campaigns and collect important data. To promote your campaign, you can add a Campaign block to a page in Page Builder or embed a campaign within the Rich Text field by using a Campaign custom tag. ![Qualifio](https://doc.ibexa.co/en/saas/ibexa_products/img/qualifio.png) ### Use cases With Ibexa Experience, your customers are the main focus of all that you do. It makes it simpler than ever to create the different touchpoints that your customers have with your brand, giving you the ability to guide them through your significant business procedures. #### Build new pages and integrated forms User interface of Ibexa Experience is intuitive and plain. With its new features - Page and Form Builder - you can build new pages or forms quickly efficiently. Page Builder comes with predefined layouts, blocks, and templates to streamline your design process, while Form Builder provides ready-to-use elements for easy form creation. You can integrate your custom forms and surveys into the website and reuse content from existing sites on new ones. With Site Factory, you can publish as many sites as you like, there are no limits. #### Target customers in their preferred channels To make your products attractive, you must remember each of your customers is unique and special, and tailor your marketing strategy to their needs and preferences. Ibexa Experience allows you to deliver personalized content and recommendations through different channels. With segmentation, you can define audiences to distribute specific content through the right channels, at the right time. Available add-ons give you even more possibilities. You can analyze customer behaviours, use interactive content, and collect important data. #### Build your future Planning and scheduling is a part of management. With scheduling tools available in Experience edition, you can prepare content publishing timetables, schedule how your website can evolve, and test or preview it before publication. Additionally, you can use editorial calendars for an easier collaboration. # Getting started # Getting started > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). Get started working with Cohesivo by taking your first steps in a new installation. To get started working with Cohesivo, see what first steps to take to familiarize yourself with the platform. - [First steps](https://ez-systems-developer-documentation--3381.com.readthedocs.build/en/3381/getting_started/first_steps/): Start off working with Cohesivo by doing initial configuration and testing system capabilities. # First steps > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). Start off working with Cohesivo by doing initial configuration and testing system capabilities. This page lists first steps you can take after installing Cohesivo. These steps are the most common actions you may need to take in a new installation. > **Tip: Beginner tutorial** > > To go through a full tutorial that leads from a clean installation to creating a full site, see [Beginner tutorial](https://doc.ibexa.co/en/saas/tutorials/beginner_tutorial/beginner_tutorial/index.md). ## Remove welcome page ![Welcome page](https://doc.ibexa.co/en/saas/getting_started/img/welcome_page.png) To remove the welcome page and get a completely clean installation to start your project with, remove the following files and folders from your installation: - Delete the file `config/packages/ibexa_welcome_page.yaml` - Delete the `templates/themes/standard/full/welcome_page.html.twig` file - Delete the `assets/scss` folder - Delete all `translations/ibexa_platform_welcome_page.*` files - In `webpack.config.js` remove the `Encore.addEntry` section and uncomment the last line, so that the end of the file looks like this: ```js module.exports = [ibexaConfig, ...customConfigs, projectConfig]; // uncomment this line if you've commented-out the above lines module.exports = [ eZConfig, ibexaConfig, ...customConfigs ]; ``` ## Add a content type 1. In your browser, go to the back office: `/admin`, and log in with the default username: `admin` using the password specified during installation. > **Caution: Password change** > > Make sure that you change the default password before you switch your installation from development to production. For more information about passwords, see [Passwords](https://doc.ibexa.co/en/saas/users/passwords/index.md). For more information about production security, see [Security checklist](https://doc.ibexa.co/en/saas/infrastructure_and_maintenance/security/security_checklist/index.md). 2. In the upper-right corner, click the avatar icon and in the drop-down menu disable the [Focus mode](https://doc.ibexa.co/projects/userguide/en/6.0/getting_started/discover_ui/#focus-mode). 3. Select content and go to content types. 4. Enter the content group and create a new content type. ![Creating a content type](https://doc.ibexa.co/en/saas/getting_started/img/first-steps-create-ct.png) 5. Input the content type's name, for example "Blog Post", and identifier: `blog_post`. 6. Below, add a field definition of the type Text Line. Name it "Title" and give it identifier `title`. 7. Add another field definition: Text (type Rich text) with identifier `text`. > **Note: Note** > > Make sure all fields are marked as *Translatable*. This setting is required to enable [content translation](#add-a-language-and-translate-content) for all fields in the created content type. 8. Save the content type. For more information, see [Content model](https://doc.ibexa.co/en/saas/content_management/content_model/index.md). ## Create Twig templates and match then with view config To display content in the front page you need to define content views and templates. Content views decide which templates and controllers are used to display content. 1. In `config/packages/ibexa.yaml`, under `ibexa.system` add the following block (pay attention to indentation: `site_group` should be one level below `system`): ```yaml site_group: content_view: full: blog_post: template: full\blog_post.html.twig match: Identifier\ContentType: [blog_post] ``` Content view templates use the [Twig templating engine](https://twig.symfony.com/). 2. Create a template file `templates/full/blog_post.html.twig`: ```html+twig

{{ ibexa_render_field(content, 'title') }}

{{ ibexa_render_field(content, 'text') }}
``` For more information, see [Templates](https://doc.ibexa.co/en/saas/templating/templates/templates/index.md) and [Twig documentation](https://twig.symfony.com/doc/3.x/). ## Create content and test view templates 1. Go to the back office, select **Content** -> **Content structure**, and create a new content item by clicking **Create content**. ![Creating a Blog Post](https://doc.ibexa.co/en/saas/getting_started/img/first-steps-create-content.png) 2. Select a Blog Post content type. Fill in the content item and publish it. 3. To preview the new content item on the front page, go to `/`. For example, if the title of the Blog post is "First blog post", the address is `/first-blog-post`. ![Previewing Content](https://doc.ibexa.co/en/saas/getting_started/img/first-steps-preview-content.png) ## Add SiteAccesses You can use SiteAccesses to serve different versions of the website. SiteAccesses are used depending on matching rules. They're set up in YAML configuration under the `ibexa.siteaccess.list` key. 1. In `config/packages/ibexa.yaml` add a new SiteAccess called `de` for the German version of the website: ```yaml ibexa: # ... siteaccess: list: [site, de] groups: site_group: [site, de] ``` The SiteAccess is automatically matched based on the last part of the URI. 2. You can now access the front page through the new SiteAccess: `/de`. > **Note: Log in** > > At this point you need to log in to preview the new SiteAccess, because an anonymous visitor doesn't have permissions to view it. See [section about permissions below](#set-up-permissions). For now the new SiteAccess doesn't differ from the main site. For more information, see [Multisite](https://doc.ibexa.co/en/saas/multisite/multisite/index.md) and [SiteAccess matchers](https://doc.ibexa.co/en/saas/multisite/siteaccess/siteaccess_matching/#available-siteaccess-matchers). ## Add a language and translate Content One of the most common use cases for SiteAccesses is having different language versions of a site. 1. To set up the `de` SiteAccess to use a different language, add its configuration under `ibexa.system`, below `site.languages`: ```yaml site: languages: [eng-GB] de: languages: - ger-DE - eng-GB ``` This means that German is used as the main language for this SiteAccess, and English as a fallback. 2. Go to the back office and select **Admin** > **Languages**. Add a new language called "German", with the language code `ger-DE`. Make sure it's enabled. ![Creating a language](https://doc.ibexa.co/en/saas/getting_started/img/first-steps-create-language.png) 3. Next, go to the **Content structure** and open the blog post you had created earlier. Switch to the **Translations** tab and add a new translation. ![Adding a translation](https://doc.ibexa.co/en/saas/getting_started/img/first-steps-add-translation.png) 4. Select German as the target language and base the translation on the English source text. Edit the content item and publish it. 5. Go to the front page. The blog post now displays different content, depending on which SiteAccess you enter it from: `/` or `/de/`. ![Previewing translated Content](https://doc.ibexa.co/en/saas/getting_started/img/first-steps-translated-content.png) For more information, see [Languages](https://doc.ibexa.co/en/saas/multisite/languages/languages/index.md) and [Set up translation SiteAccess](https://doc.ibexa.co/en/saas/multisite/set_up_translation_siteaccess/index.md). ## Add a design The design engine enables you to use different themes consisting of templates and assets. Each theme is stored in a separate folder and assigned to a SiteAccess. To create a new theme: 1. Add the following configuration at the bottom of `config/packages/ibexa.yaml` (at the same level as `ibexa`): ```yaml ibexa_design_engine: design_list: site_design: [site_design] de_design: [de_design] ``` 2. In configuration of the `de` SiteAccess (under `ibexa.system.de`) add: `design: de_design` 3. Under `site`, add `design: site_design` 4. Go back to the `content_view` configuration for the blog post. Change the path to the template so that it points to the folder for the correct design: `template: '@ibexadesign\full\blog_post.html.twig'` This means that the app looks for the `blog_post.html.twig` file in a folder relevant for the SiteAccess: `de_design` for the `de` SiteAccess, or `site_design` for other SiteAccesses in `site_group`. 5. Create a `themes` folder under `templates`, and two folders under it: `de_design` and `site_design`. 6. Move the existing `full\blog_post.html.twig` file under `site_design`. 7. Copy it also under `de_design`. Modify the second one in any way (for example, add some html), so you can preview the effect. 8. To see the difference between the different themes, compare what is displayed at `/` and `/de/` ## Set up permissions To allow a group of users to edit only a specific content type (in this example, blog posts), you need to set up permissions for them. Users and user groups are assigned roles. A role can contain a number of policies, which are rules that permit the user to perform a specific function. Policies can be additionally restricted by limitations. 1. Go to **Admin** -> **Users**. Create a new user group (the same way you create regular content). Call the group "Bloggers". 2. In the new group create a user. Remember their username and password. Mark the user as "Enabled". ![Creating a User](https://doc.ibexa.co/en/saas/getting_started/img/first-steps-create-user.png) 3. Go to **Admin** -> **Roles**. Create a new role called "Blogger". 4. Add the following policies to ensure the user can log in and access content: - `User/Login` - `Content/Read` - `Content/Versionread` - `Section/View` - `Content/Reverserelatedlist` When creating these policies, don't add any limitations and click **Save** to proceed. 5. Now add policies that allow the user to create and publish content, limited to Blog Posts: - `Content/Create` with limitation for content type Blog Post - `Content/Edit` with limitation for content type Blog Post - `Content/Publish` with limitation for content type Blog Post ![Adding limitations to a policy](https://doc.ibexa.co/en/saas/getting_started/img/first-steps-policy-limitations.png) 6. In the **Assignments** tab assign the "Blogger" role to the "Bloggers" group. ![Assigning a role](https://doc.ibexa.co/en/saas/getting_started/img/first-steps-assign-roles.png) You can now log out and log in again as the new user. You're able to create Blog Posts only. For more information, see [Permissions](https://doc.ibexa.co/en/saas/permissions/permissions/index.md). # Tutorials # Tutorials > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). Get started with tutorials to learn how to create a site with Cohesivo Get started with tutorials to learn how to create a site with Cohesivo. > **Note: Note** > > Remember that each tutorial should be performed on a clean project to avoid conflicts with added or modified files. - [Beginner tutorial](https://ez-systems-developer-documentation--3381.com.readthedocs.build/en/3381/tutorials/beginner_tutorial/beginner_tutorial/): Go through a beginner tutorial which presents the Cohesivo content model and show how to configure and use templates to create a basic site. - [Page and Form tutorial](https://ez-systems-developer-documentation--3381.com.readthedocs.build/en/3381/tutorials/page_and_form_tutorial/page_and_form_tutorial/): Go through a Page and Form tutorial to learn how to create modular Sites and how to manage forms and their submissions. - [Creating a Point 2D field type](https://ez-systems-developer-documentation--3381.com.readthedocs.build/en/3381/tutorials/generic_field_type/creating_a_point2d_field_type/): Go through a field type tutorial to learn how to create a custom field type based on the built-in Generic field type. # Beginner tutorial > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). Go through a beginner tutorial which presents the Cohesivo content model and show how to configure and use templates to create a basic site. This tutorial is a step-by-step guide to building a Cohesivo website. You can use it with both Ibexa Headless and Ibexa Experience. ## Intended audience The tutorial is intended for users who have little or no previous experience with Cohesivo. To follow it, you should: - Have basic knowledge of HTML and CSS. - Have basic knowledge of the database you've selected. ## Learning outcomes After finishing this tutorial, you should: - know how to construct the content model of a website. - be able to use templates to display your content according to your needs. - know how to manage users and permissions. ## Scenario In the course of this tutorial you can build a website for storing and sharing bike rides. It enables the user to add information and photos of their routes and indicate what interesting points can be visited during the trip. ## Steps In this tutorial you go through the following steps: 1. [Get ready](https://doc.ibexa.co/en/saas/tutorials/beginner_tutorial/1_get_ready/index.md) 2. [Create the content model](https://doc.ibexa.co/en/saas/tutorials/beginner_tutorial/2_create_the_content_model/index.md) 3. [Customize the front page](https://doc.ibexa.co/en/saas/tutorials/beginner_tutorial/3_customize_the_front_page/index.md) 4. [Display a single content item](https://doc.ibexa.co/en/saas/tutorials/beginner_tutorial/4_display_single_content_item/index.md) 5. [Display a list of content items](https://doc.ibexa.co/en/saas/tutorials/beginner_tutorial/5_display_a_list_of_content_items/index.md) 6. [Improve configuration](https://doc.ibexa.co/en/saas/tutorials/beginner_tutorial/6_improve_configuration/index.md) 7. [Embed content](https://doc.ibexa.co/en/saas/tutorials/beginner_tutorial/7_embed_content/index.md) 8. [Enable account registration](https://doc.ibexa.co/en/saas/tutorials/beginner_tutorial/8_enable_account_registration/index.md) # Step 1 — Get ready > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). Start the tutorial by getting a clean installation of Cohesivo. To begin the tutorial, you need a clean installation of Cohesivo. The clean installation contains only a root content item which displays a welcome page. ![Front page after clean installation](https://doc.ibexa.co/en/saas/getting_started/img/welcome_page.png) You can replace the welcome page with your own in step 3. To remove it for now, go to `config/packages/` and delete the `ibexa_welcome_page.yaml` file. You can now start creating the content model. # Step 2 — Create the content model > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). Learn how to create a content model consisting of content types and a few sample content items. How your content is structured is an important part of a Cohesivo project. Think of it as the database design of your application. To get full information, read the [content model](https://doc.ibexa.co/en/saas/content_management/content_model/index.md) documentation page. Below is a short introduction that only covers points needed for this tutorial. ## Content model overview The Cohesivo content repository is centered around **content items**. A content item is a single piece of content, for example an article, a product review, a place, and more. Every content item is an instance of a content type. Content types define what **Fields** are included in each content item. For example, an article could include fields such as *title*, *image*, *abstract*, *article's body*, *publication date* and *list of authors*. Fields can belong to one of the installed **field types**, about 30 in the default distribution. Each field type is built to represent a specific type of data: a text line, a block of rich text, an image, a collection of relations to content items, and more. You can find a complete list in the [field types reference](https://doc.ibexa.co/en/saas/content_management/field_types/field_type_reference/field_type_reference/index.md) section. Every field type may have its own options, and comes with its own editing and viewing interfaces. ## Add a content type The site use two content types: **Ride** and **Landmark**. A Ride is a route of a bike trip. It can include one or more Landmarks - interesting places you can see along the way. More than one Ride can visit the same Landmark, so it's similar to an N-N relationship model in a database. In this step you add the first content type, Ride. Go to the admin interface (`/admin`) and log in with the default username: `admin` using the password specified during installation. In the upper-right corner, click the avatar icon to unfold the drop-down menu and disable the [Focus mode](https://doc.ibexa.co/projects/userguide/en/6.0/getting_started/discover_ui/#focus-mode). In the main menu, go to **Content** -> **Content types**. You can see a list of **Content type groups**. They're used to group content types in a logical way. Select **Content** and then click the **Create** button. ![Add a content type button](https://doc.ibexa.co/en/saas/tutorials/beginner_tutorial/img/bike_tutorial_create_content_type.png) Fill the form with this basic info: - **Name**: Ride - **Identifier**: `ride` Then create all fields with the following information: | Field type | Name | Identifier | Required | Searchable | Translatable | | ------------ | -------------- | ---------------- | -------- | ---------- | ------------ | | Text line | Name | `name` | yes | yes | yes | | Image Asset | Photo | `photo` | no | no | no | | Rich text | Description | `description` | yes | yes | yes | | Map location | Starting point | `starting_point` | yes | yes | no | | Map location | Ending point | `ending_point` | yes | yes | no | | Integer | Length | `length` | yes | yes | no | Confirm the creation of the content type by clicking **Save and close**. ## Create Rides > **Note: Note** > > If you're using Ibexa Experience, the root content item in your installation is a Page called "Ibexa Digital Experience Platform". > > For this tutorial, swap it with its child, a Folder called "Ibexa Platform". > > To do this, in the main menu go to **Content** -> **Content structure** -> **Ibexa Digital Experience Platform**, select the **Locations** tab and in the **Swap Locations** section navigate to "Ibexa Platform". > > You can learn how to work with Pages in [another tutorial](https://doc.ibexa.co/en/saas/tutorials/page_and_form_tutorial/page_and_form_tutorial/index.md). Go back to the content by selecting **Content structure** in the main menu. Then browse the content tree and create a Folder named *All Rides* by clicking the **Create content** button on the top right of the screen. Publish the Folder. While in the folder, create a few of Rides using the **Create content** button, add photos and publish them. ![Ready for Step 3](https://doc.ibexa.co/en/saas/tutorials/beginner_tutorial/img/bike_tutorial_all_rides_admin.png) Once you have two or more Rides in the Folder, you're ready to customize the homepage of the website. # Step 3 — Customize the front page > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). Try customizing the front page by using custom templates and adding assets. In this step you can create the global layout of your site, and display content by using custom templates. First, go to the root of the site (``). You should now see the home page of the clean install, without any kind of layout. You can customize this step by instructing the platform to use a custom template to render this content item. ## Content rendering configuration To use a custom template when rendering the root content, create a `content_view` configuration block for `ibexa`. > **Note: Note** > > When pasting YAML code, pay attention to indentation and levels. The code blocks shown here include the full structure of the YAML file to help you learn where to place new blocks. Be careful not to duplicate existing keys, because YAML doesn't allow it. Edit `config/packages/ibexa.yaml`. Add the following block under `site` while paying attention to indentation - `content_view` should be one level below `site`: ```yaml ibexa: system: site: content_view: full: home_page: template: full/home_page.html.twig match: Id\Location: 2 ``` This tells Cohesivo to use the `template` when rendering content with Location ID `2`. `2` is the default location for the root content item. `Id\Location` is one of several [view matchers](https://doc.ibexa.co/en/saas/templating/templates/view_matcher_reference/index.md) that you can use to customize rendering depending on different criteria. > **Note: Clear the cache** > > Each time you change the YAML files, you should clear the cache. It's not mandatory in dev environment. > > To clear the cache: > > ```bash > php bin/console cache:clear > ``` ## Create template and layout ### Create the first template Next, you need to create the template that you indicated in configuration. For the time being, fill the template with a basic "Hello world" message. Create a `home_page.html.twig` file in `templates/full/`: ```html+twig

Hello World!

``` Refresh the page to see an unstyled version of the message. > **Note: Note** > > If you still see the welcome page, go to `config/packages/` and make sure you deleted the `ibexa_welcome_page.yaml` file. ### Add the site's main layout Most sites have a general layout which includes things like header with a logo or footer. It's displayed on every page, and the content of the page is placed inside it. To add a template like this to your site, create a `main_layout.html.twig` file in `templates/` and paste the following code into it: ```html+twig Cohesivo Beginner Tutorial {{ encore_entry_link_tags('tutorial') }}
{% block content %} {% endblock %}
{{ encore_entry_script_tags('tutorial-js') }} ``` In the highlighted lines (12 and 89) the template takes advantage of [Symfony Webpack Encore](https://symfony.com/doc/7.4/frontend.html#webpack-encore). This tutorial leads you through configuring Webpack, but first you need assets. ### Adding assets The site has no stylesheets or assets yet. You need to download [`assets.zip`](https://doc.ibexa.co/en/saas/tutorials/beginner_tutorial/img/assets.zip) which contains the prepared asset files. Then unpack its contents to the following directories: - `css`, `fonts`, and `js` folders to `assets/` - `images` folder to `public/assets/` Before proceeding, ensure that the structure of the added files looks like this: ![File structure](https://doc.ibexa.co/en/saas/tutorials/beginner_tutorial/img/bike_tutorial_listing_web_v3.png) ### Configuring Webpack In Cohesivo, you can add assets by using [Symfony Webpack Encore](https://symfony.com/doc/7.4/frontend.html#webpack-encore) — an integration of Webpack that enables you to build bundles of CSS stylesheets and JS scripts and add them to the project. For more information, see [Importing assets from a bundle](https://doc.ibexa.co/en/saas/administration/back_office/back_office_elements/importing_assets_from_bundle/index.md). To create bundles, first, indicate which files to include in them. Open the `webpack.config.js` file located in the root folder of your project. Paste the following code right under `Encore.addEntry('app', './assets/app.js');`: ```javascript Encore .addStyleEntry('tutorial', [ path.resolve(__dirname, './assets/css/normalize.css'), path.resolve(__dirname, './assets/css/bootstrap.min.css'), path.resolve(__dirname, './assets/css/bootstrap-theme.css'), path.resolve(__dirname, './assets/css/style.css') ]) .addEntry('tutorial-js', [ path.resolve(__dirname, './assets/js/bootstrap.min.js') ]); ``` `.addStyleEntry('tutorial', [])` and `.addEntry('tutorial-js', [])` refer to `{{ encore_entry_link_tags('tutorial') }}` and `{{ encore_entry_script_tags('tutorial-js') }}` from `main_layout.html.twig`. This configuration creates a bundle consisting of files to be added to a template. At this point the bundles are created and ready to be used. ### Extending templates Now you have to add the `main_layout.html.twig` template that uses the assets to the `home_page.html.twig` template. To add one template to another, edit `templates/full/home_page.html.twig` and replace it with the following code: ```html+twig {% extends "main_layout.html.twig" %} {% block content %}

Hello World!

{% endblock %} ``` The templating language Twig supports [template inheritance](https://twig.symfony.com/doc/3.x/tags/extends.html). Templates can contain named blocks. Any template can extend other templates, and modify the blocks defined by its parents. The code above points to `main_layout.html.twig` in line 1. It also wraps your "Hello world" message in a `content` block. If you look back at the main layout template, you can see an empty `{% block content %}{% endblock %}` section (lines 52-53). This is where the `home_page.html.twig` is rendered. Clear the cache and regenerate the assets by running the following commands: ```bash php bin/console cache:clear php bin/console assets:install yarn encore ``` > **Tip: Tip** > > You should run the `yarn encore` command with the [environment](https://doc.ibexa.co/en/saas/infrastructure_and_maintenance/environments/index.md) you're using. > > By default, Cohesivo installs in the dev environment. If you changed it to prod, use `yarn encore prod`. Refresh the page and you should see the "Hello world" placed inside a styled layout. ![Homepage with a Hello world](https://doc.ibexa.co/en/saas/tutorials/beginner_tutorial/img/bike_tutorial_hello_world.png) At this point, the template is static. It doesn't render any dynamic data from the repository. # Step 4 — Display a single content item > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). Learn how to render content details with a custom template. You render a list of all Rides here in the next step. But before that, you can use the existing page layout to render the content of a single Ride. ## Create the Ride view Create a Twig template `templates/full/ride.html.twig` with the following code: ```html+twig {% extends "main_layout.html.twig" %} {% block content %}

{{ content.name }}

{{ 'Starting point'|trans }}

{{ ibexa_render_field(content, 'starting_point', {'parameters': { 'width': '100%', height: '200px', 'showMap': true, 'showInfo': false }}) }}

{{ 'Ending point'|trans }}

{{ ibexa_render_field(content, 'ending_point', {'parameters': { 'width': '100%', height: '200px', 'showMap': true, 'showInfo': false }}) }}

{{ ibexa_render_field( content, 'length') }} km

{{ 'Description'|trans }}

{{ ibexa_render_field( content, 'description') }}
{% endblock %} ``` This template reuses `main_layout.html.twig` and again places the template in a `content` block. > **Tip: Previewing available variables** > > You can see what variables are available in the current template with the `dump()` Twig function: > > ```html+twig > {{ dump() }} > ``` > > You can also dump a specific variable: > > ```html+twig > {{ dump(location) }} > ``` Now you need to indicate when this template should be used. Go back to `config/packages/ibexa.yaml` and add the following configuration (under the existing `content_view` and `full` keys:): ```yaml site: content_view: full: # existing keys, don't change them ride: template: full/ride.html.twig match: Identifier\ContentType: ride ``` This tells the application to use this template whenever it renders the full view of a Ride. ## Check the Ride full view Because you don't have a list of Rides on the front page yet, you cannot click a Ride to preview it. But you still can see how the template works in two ways: ### Preview in the back office You can use the [preview](https://doc.ibexa.co/projects/userguide/en/6.0/content_management/preview_content_items/) while editing in the back office to see how the content is rendered in full view. ![Full ride preview in admin](https://doc.ibexa.co/en/saas/tutorials/beginner_tutorial/img/bike_tutorial_preview_full_ride.png) ### Go to the Ride page You can also go directly to the URL of a Ride. The URL for a Ride content item located in the "All Rides" Folder is `http:///all-rides/`. # Step 5 — Display a list of content items > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). Learn how to query for content and render it in a list. Now that you know how to display a single content item, you can take care of rendering a list of content items. In this step you can display a table of all Rides on the front page. The pagination uses default styling, as this tutorial focuses on functionality. ## Customize the homepage template In `templates/full/home_page.html.twig` replace the "Hello world" with a table that displays the list of all existing Rides: ```html+twig {% extends "main_layout.html.twig" %} {% block content %}
{% for ride in rides.currentPageResults %} {{ render( controller( 'ibexa_content::viewAction', { 'location': ride.valueObject, 'viewType': 'line' } )) }} {% endfor %}
{{ 'Ride'|trans }} {{ 'From'|trans }} {{ 'To'|trans }} {{ 'Distance'|trans }}
{% if rides.haveToPaginate() %}
{% endif %}
{% endblock %} ``` The `rides` variable you use in line 15 above needs to contain a list of all Rides. To get this list, you use a Query Type. ## Create a QueryType for the home page QueryType objects are used to limit and sort results for content item queries. For more information, see [Built-In Query Types](https://doc.ibexa.co/en/saas/templating/queries_and_controllers/built-in_query_types/index.md). Here, you need to display `ride` objects that have been published (are visible). Create a `RideQueryType.php` file in `src/QueryType`: ```php $parameters */ public function getQuery(array $parameters = []): \Ibexa\Contracts\Core\Repository\Values\Content\LocationQuery { return new LocationQuery([ 'filter' => new Criterion\LogicalAnd( [ new Criterion\Visibility(Criterion\Visibility::VISIBLE), new Criterion\ContentTypeIdentifier(['ride']), ] ), ]); } public function getSupportedParameters(): array { return []; } } ``` This Query Type finds all visible content items that belong to the `ride` content type (lines 21-22). Now you need to indicate that this Query Type is used in your configuration. ## Add Query Type to configuration Edit `config/packages/ibexa.yaml`. In the view configuration for the home page indicate that this view uses the Query Type: ```yaml site: content_view: full: # existing keys, don't change them home_page: controller: ibexa_query::pagingQueryAction template: full/home_page.html.twig match: Id\Location: 2 params: query: query_type: Ride limit: 4 assign_results_to: rides ``` The `query_type` parameter in line 12 indicates which Query Type to use. You defined the name `Ride` in the Query Type file in the `getName` method. Using the `pagingQueryAction` of the built-in `ibexa_query` controller (line 6) enables you to automatically get paginated results. You can set the limit of results per page in the `limit` parameter. ### View types So far you have been using the `full` view type to render the Ride's full view. Here, on the other hand, you use the `line` view, as indicated by `'viewType': 'line'` in the home page template (line 16). You can configure custom view types with any name you want, as long as you include them in the configuration. Let's do this now with the `line` view for Rides. ## Create a line template for Rides Add a rule for the `ride` template in your `config/packages/ibexa.yaml` file. `line` should be at the same level as `full`. ```yaml system: site: content_view: line: ride: template: line/rides.html.twig match: Identifier\ContentType: ride ``` Create the `templates/line/rides.html.twig` template. Because this template is rendered inside a table, it starts with a `` tag. ```html+twig {{ content.name }} {% if not ibexa_field_is_empty( content, 'photo' ) %} {{ ibexa_render_field(content, 'photo') }} {% endif %} {{ ibexa_render_field(content, 'starting_point', {'parameters': {'width': '100%', 'height': '100px', 'showMap': true, 'showInfo': true }}) }} {{ ibexa_render_field(content, 'ending_point', {'parameters': {'width': '100%', 'height': '100px', 'showMap': true, 'showInfo': true }}) }}

{{ ibexa_render_field( content, 'length' ) }} Km

``` ### Media permission To be able to view the `photo` field you need to have a `content/read` permission to `Media` section. To verify that you have this permission, in the main menu, go to **Admin** (gear icon) -> **Roles**, and click the **Anonymous** role. If needed, edit the **Content/Read** policy line to add the `Media` section to **Limitation** along with the `Standard` section. ![Policies for the Anonymous Role with Media section](https://doc.ibexa.co/en/saas/tutorials/beginner_tutorial/img/step5_admin_anonymous_policies_with_media_section.png) Now go to the homepage of your website and you can see the list of Rides. However, the Ride photos are too large and stretch the table. In the next step you can ensure they're displayed in proper size. # Step 6 — Improve configuration > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). See how you can manage Cohesivo configuration files. ## Define image variations Image variations are different versions of the same image. You can use them, for example, to scale images, crop them, or add effects. So far the images in the ride list are fitted to the templates automatically, and the result doesn't look good. Now you can create a variation to specify how you want the images to look in detail. Create a new `config/packages/image_variations.yaml` file containing: ```yaml ibexa: system: default: image_variations: ride_list: reference: null filters: - {name: geometry/scaledownonly, params: [140, 100]} ``` Next, modify the templates to use these variations. Variation names are provided as parameters when rendering the image content. In `templates/line/rides.html.twig` add the `'alias': 'ride_list'` parameter in the following way, in lines 8-10: ```html+twig {% if not ibexa_field_is_empty( content, 'photo' ) %} {{ ibexa_render_field(content, 'photo', { 'parameters': { 'alias': 'ride_list' } }) }} {% endif %} ``` This ensures that the photo displayed next to each Ride in the list is scaled down properly with proportions retained. Clear cache and refresh the front page. Photos should now have a regular size and fit in the table. ![Ride list with proper image variations](https://doc.ibexa.co/en/saas/tutorials/beginner_tutorial/img/bike_tutorial_ride_list.png) ## Separate view configuration In a larger site there are many elements that need configuration. To keep it more organized, you can split parts of configuration into separate files. As an example, you can separate all content view configuration into its own file. Create a `config/packages/views.yaml` file. Copy everything under `content_view` from `config/packages/ibexa.yaml` and move it to the new file. Remove the corresponding code from `ibexa.yaml`. The `views.yaml` file should look like this: ```yaml ibexa: system: site: content_view: full: home_page: controller: ibexa_query::pagingQueryAction template: full/home_page.html.twig match: Id\Location: 2 params: query: query_type: Ride limit: 4 assign_results_to: rides ride: template: full/ride.html.twig match: Identifier\ContentType: ride line: ride: template: line/rides.html.twig match: Identifier\ContentType: ride ``` # Step 7 — Embed content > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). Learn how to embed related content in another content item's template. Creating lists and detailed views of content types and their respective items often involves loading related resources. In this step, you add a related object, a Landmark, which is displayed on Ride pages. You can add as many or as little related resources as you like. ## Add the Landmark content type Now you need to add the second content type needed in the site, Landmark. Go to **Content types**, and in the **Content** group, add the Landmark content type. A Landmark is an interesting place that Rides go through. Each Ride may be related to multiple Landmarks. - **Name**: Landmark - **Identifier**: landmark Then add all fields with the following information: | Field type | Name | Identifier | Required | Searchable | Translatable | | ------------ | ----------- | ------------- | -------- | ---------- | ------------ | | Text line | Name | `name` | yes | yes | yes | | Rich text | Description | `description` | no | yes | yes | | Image Asset | Photo | `photo` | yes | no | no | | Map location | Location | `location` | yes | yes | no | Confirm the creation of the content type by selecting **Create**. Create a *Landmarks* Folder and add some Landmarks to it. You need pictures (for the Photo field) to represent them. ## Add Landmarks to Ride content type definition Now edit the Ride content type to add a Multiple Content Relation between the two content types. Create a new **Content relations (multiple)** field called "Landmarks" with identifier `landmarks` and allow content type "Landmark" to be added to it: ![Adding Landmarks to the Ride content type](https://doc.ibexa.co/en/saas/tutorials/beginner_tutorial/img/bike_ride_adding_landmarks_to_the_ride_content_type.png "Adding a relation between the Ride and the Landmark using Content Relations (multiple)") Confirm by clicking **Save and close**. Go back to one of your existing Rides, edit it and link some Landmarks to it. Click **Publish**. ## Display a list of Landmarks in Ride view ### Create Landmark line view Now you need to create the line view for Landmarks. Declare a new override rule in `config/packages/views.yaml`: ```yaml ibexa: system: site: content_view: #full views here line: landmark: template: line/landmark.html.twig match: Identifier\ContentType: landmark ``` Add the template for the line view of a Landmark by creating `templates/line/landmark.html.twig`: ```html+twig
{# MODAL #}
``` Like before, you use an image variation here (line 4) and you need to configure it. Add the following section to `config/packages/image_variations.yaml`, at the same level as `ride_list`: ```yaml landmark_list: reference: null filters: - {name: geometry/scalewidth, params: [200]} ``` ### Create the RideController You must provide additional information when the Ride object is displayed. This requires creating a custom controller. The controller uses `ContentService` to load related resources (Landmarks) for a particular Ride. Create a `src/Controller/RideController.php` file: ```php getContent(); $landmarksListId = $currentContent->getFieldValue('landmarks'); $landmarksList = []; foreach ($landmarksListId->destinationContentIds as $landmarkId) { $landmarksList[$landmarkId] = $this->contentService->loadContent($landmarkId); } $view->addParameters(['landmarksList' => $landmarksList]); return $view; } } ``` Update `config/packages/views.yaml` to mention the `RideController.php` by adding a line with the `controller` key to the view config: ```yaml ibexa: system: site: content_view: full: ride: template: full/ride.html.twig controller: App\Controller\RideController::viewRideWithLandmarksAction match: Identifier\ContentType: ride ``` ### Add the Landmark in the Ride full view Now modify the Ride full view template to include a list of Landmarks, and the controller that you created. Add the following lines at the end of `templates/full/ride.html.twig`, before the last `` and the closing tag `{% endblock %}`: ```html+twig {% if landmarksList is not empty %}

{{ 'Landmarks'|trans }}

{% for landmark in landmarksList %} {{ render( controller( "ibexa_content::viewAction", { 'content': landmark, 'viewType': 'line'} )) }} {% endfor %}
{% endif %} ``` You can now check the Ride page again to see all the connected Landmarks. > **Tip: Tip** > > You can use `dump()` in Twig templates to display all available variables. ![Ride full view with Landmarks](https://doc.ibexa.co/en/saas/tutorials/beginner_tutorial/img/bike_tutorial_ride_with_landmarks.png) # Step 8 — Enable account registration > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). See how you can enable external users to register and contribute to your site. In this step you enable other users to create accounts on your site, access the back office and create content. ## Enable registration In the main menu, go to **Admin** (gear icon) -> **Roles**, and click the **Anonymous** role. ![Available roles](https://doc.ibexa.co/en/saas/tutorials/beginner_tutorial/img/step_8_role_mgmt_screen.png) Add the `User/Register` policy to the Anonymous user. This allows any visitor to the website to access the registration form. ![Policies for the Anonymous Role](https://doc.ibexa.co/en/saas/tutorials/beginner_tutorial/img/step8_admin_anonymous_policies.png) Then go to `/register`. The registration form is unstyled, so you need to add templates to it. ## Customize registration forms In the `config/packages/views.yaml` file add a `user_registration` key under `site`, at the same level as `content_view`: ```yaml ibexa: system: site: # existing content_view keys user_registration: templates: form: user/registration_form.html.twig ``` This indicates which template is used to render the registration form. Create the file `templates/user/registration_form.html.twig`: ```html+twig {% extends "main_layout.html.twig" %} {% block page_head %} {% set title = 'Register user'|trans %} {{ parent() }} {% endblock %} {% block content %} {% import 'user/registration_content_form.html.twig' as registrationForm %}

{{ 'Member Registration'|trans }}

* {{ 'All fields are required'|trans }}
{{ registrationForm.display_form(form) }}
{% endblock %} ``` In line 10 you can see that another file is imported: `registration_content_form.html.twig`. The second template renders the actual fields of the registration form. Create this file as well (as `templates/user/registration_content_form.html.twig`): ```html+twig {% macro display_form(form) %} {{ form_start(form) }} {% for fieldForm in form.fieldsData %} {% set fieldIdentifier = fieldForm.vars.data.fieldDefinition.identifier %} {% if fieldIdentifier == 'first_name' or fieldIdentifier == 'last_name' %} {% if fieldIdentifier == 'first_name' %}
{% endif %}
{{ form_errors(fieldForm.value) }} {{ form_widget(fieldForm.value, { 'contentData': form.vars.data }) }}
{% if fieldIdentifier == 'last_name' %}
{% endif %} {% endif %} {% if fieldIdentifier == 'user_account' %}
{{ form_widget(fieldForm.value, { 'contentData': form.vars.data }) }}
{% endif %} {%- do fieldForm.setRendered() -%} {% endfor %}
{{ form_widget(form.register, {'attr': { 'class': 'btn btn-block btn-primary' }}) }}
{{ form_end(form) }} {% endmacro %} ``` The third template you need to prepare covers the confirmation page that is displayed when a user completes the registration. First, point to the new template in the configuration. Add a `confirmation` key to `config/packages/views.yaml`: ```yaml user_registration: templates: form: user/registration_form.html.twig confirmation: user/registration_confirmation.html.twig ``` Then create the `templates/user/registration_confirmation.html.twig` template: ```html+twig {% extends "main_layout.html.twig" %} {% block page_head %} {% set title = 'Registration complete'|trans %} {{ parent() }} {% endblock %} {% block content %}

{{ 'Registration completed'|trans }}

{{ 'You\'re all set up and ready to go'|trans }}
{% endblock %} ``` ## Add policy In the main menu, go to **Admin** (gear icon) -> **Roles**, and click the **Anonymous** role. Add the `Content/Create` policy to the Anonymous user. This allows users to fill in the registration form. Now return to `/register`: ![Complete Register page with the layout](https://doc.ibexa.co/en/saas/tutorials/beginner_tutorial/img/step8_register_page.png) Fill in the form and register a user. > **Tip: Tip** > > If you log in as the new user at this point, you need to go to the back office (`/admin`) to log out again re-log in as Admin. ## Set up Permissions Users created through the registration form are placed in the default user group (*Guest accounts* for Ibexa Headless and Ibexa Experience, *Customers* for Ibexa Commerce edition). The user you created has the roles assigned to this group. > **Tip: Tip** > > You can change the group in which new users are placed (but you don't need to do it for this tutorial). For more information, see [Registering new users](https://doc.ibexa.co/en/saas/users/user_registration/index.md). At this point you don't want anyone who registers to be able to add content to the website. That's why you need to create a new user group with additional permissions. When the administrator accepts a new user, they can move them to this new group. ### Create a user group In the back office, go to **Admin** -> **Users**, click the **Create content** button and create a user group named `Go Bike Members`. ### Create a Folder for contributed Rides Go to the `All Rides` Folder and create inside it a new Folder named `Member Rides`. Go Bike Members are only able to create Content in this Folder. ### Set permissions for Go Bike Members From Admin in the **Roles** screen, create a new role named *Contributors*. Now add the following policies to the Contributors role. - User/Login - User/Password - Content/Read - Content/Versionread - Content/Create with limitations: content type limited to Ride and Landmark content types and subtree to the `Member Rides` - Content/Publish with limitations: content type limited to Ride and Landmark content types and subtree to the `Member Rides` - Content/Edit with limitation: Owner limited to `Self` - Section/View - Content/Reverserelatedlist > **Tip: Tip** > > The limitations are a powerful tool for fine-tuning the permission management of the users. See [the documentation about limitations for more technical details](https://doc.ibexa.co/en/saas/permissions/limitation_reference/#content-type-group-limitation). Once the policies are set, go to the **Assignments** tab and assign the role to the user group *Go Bike Members*. Next, go to the users page. Select the user you created and move them into the *Go Bike Members* user group. ### Create content as a Go Bike Member Log out as admin and then log in again into the back office with the credentials of the new user. You now have the ability to create new Rides and Landmarks in the selected folder. ## Congratulations! Now you have created your first website with Cohesivo. **You learned how to:** - create a content model - organize files in a Cohesivo project - configure views for different content types - add assets to a Cohesivo project - use and configure Webpack Encore - use Twig templates and controllers to display content - enable user registration - manage user permissions # Page and Form tutorial > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). Go through a Page and Form tutorial to learn how to create modular Sites and how to manage forms and their submissions. Editions: Experience This tutorial is a step-by-step guide to building an advanced website with Ibexa Experience. It focuses on creating a front page using a feature called **Page Builder**. ## Intended audience This tutorial is intended for users who have basic knowledge of Cohesivo. Ideally, you should be familiar with the concepts covered in the [Beginner tutorial](https://doc.ibexa.co/en/saas/tutorials/beginner_tutorial/beginner_tutorial/index.md). ## Learning outcomes After finishing this tutorial, you: - have a working knowledge of the Page functionality and architecture - are able to create a Page and customize its layout - are able to prepare and customize Page blocks - are able to create a custom block - know how to use Form Builder and configure your form - know how to apply custom styling to blocks ## Scenario In the course of this scenario you can build a website for a magazine for dog owners called 'It's a Dog's World'. You can create a welcome page that showcases the magazine's three most important types of content: articles, dog breed information and tips. You do this by means of a Page, making use of its specific blocks, and crafting your own as well. ![It's a Dog's World - final result](https://doc.ibexa.co/en/saas/tutorials/page_and_form_tutorial/img/enterprise_tut_main_screen.png "It's a Dog's World - final result") ## Steps In this tutorial you go through the following steps: 1. [Get a starter website](https://doc.ibexa.co/en/saas/tutorials/page_and_form_tutorial/1_get_a_starter_website/index.md) 2. [Prepare the Page](https://doc.ibexa.co/en/saas/tutorials/page_and_form_tutorial/2_prepare_the_landing_page/index.md) 3. [Use existing blocks](https://doc.ibexa.co/en/saas/tutorials/page_and_form_tutorial/3_use_existing_blocks/index.md) 4. [Create a custom block](https://doc.ibexa.co/en/saas/tutorials/page_and_form_tutorial/4_create_a_custom_block/index.md) 5. [Create a newsletter form](https://doc.ibexa.co/en/saas/tutorials/page_and_form_tutorial/5_create_newsletter_form/index.md) # Step 1 — Get a starter website > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). Start the tutorial by getting a clean installation of Ibexa Experience and preparing initial content. Editions: Experience To set up the starter website, you need to follow these steps: ## Get a clean Cohesivo installation To begin the tutorial, you need a clean installation of Ibexa Experience. ## Add content types Log in to the back office – add `/admin` to your installation's address (`/admin`) and log in as `admin` user using the password specified during installation. Disable the Focus mode, go to content types screen and in the Content group add two content types with the following settings: ### Dog Breed - **Name:** Dog Breed - **Identifier:** `dog_breed` - **Fields:** | Field type | Name | Identifier | Required | Searchable | Translatable | | ----------- | ----------------- | ------------------- | -------- | ---------- | ------------ | | Text line | Name | `name` | yes | yes | yes | | Text line | Short Description | `short_description` | yes | yes | yes | | Image Asset | Photo | `photo` | yes | no | no | | RichText | Full Description | `full_description` | yes | yes | yes | ### Tip - **Name:** Tip - **Identifier:** `tip` - **Fields:** | Field type | Name | Identifier | Required | Searchable | Translatable | | ---------- | ----- | ---------- | -------- | ---------- | ------------ | | Text line | Title | `title` | yes | yes | yes | | Text block | Body | `body` | no | no | yes | ### Modify existing Article content type You also need to modify the built-in Article content type. It makes inserting photos into articles easier. Edit it to remove the Image field that has a Content Relation (ibexa_object_relation) type, and create a new field in its place: | Field type | Name | Identifier | Required | Searchable | Translatable | | ----------- | ----- | ---------- | -------- | ---------- | ------------ | | Image Asset | Image | `image` | yes | no | no | ![New image field in the Article content type](https://doc.ibexa.co/en/saas/tutorials/page_and_form_tutorial/img/enterprise_tut_image_in_article_ct.png) ## Add template, configuration and style files > **Tip: Tip** > > For an introduction on how to use templates in Cohesivo, see [Beginner tutorial](https://doc.ibexa.co/en/saas/tutorials/beginner_tutorial/beginner_tutorial/index.md). First, to remove the welcome page, go to `config/packages/` and delete the `ibexa_welcome_page.yaml` file. Place the [`pagelayout.html.twig`](https://github.com/ibexa/documentation-developer/blob/6.0/code_samples/tutorials/page_tutorial_starting_point/templates/pagelayout.html.twig) and [`pagelayout_menu.html.twig`](https://github.com/ibexa/documentation-developer/blob/6.0/code_samples/tutorials/page_tutorial_starting_point/templates/pagelayout_menu.html.twig) files in the `templates` folder. Create a new folder, called `full`, in `templates`. Place further template files in it: - [`article.html.twig`](https://github.com/ibexa/documentation-developer/blob/6.0/code_samples/tutorials/page_tutorial_starting_point/templates/full/article.html.twig) - [`dog_breed.html.twig`](https://github.com/ibexa/documentation-developer/blob/6.0/code_samples/tutorials/page_tutorial_starting_point/templates/full/dog_breed.html.twig) - [`folder.html.twig`](https://github.com/ibexa/documentation-developer/blob/6.0/code_samples/tutorials/page_tutorial_starting_point/templates/full/folder.html.twig) - [`tip.html.twig`](https://github.com/ibexa/documentation-developer/blob/6.0/code_samples/tutorials/page_tutorial_starting_point/templates/full/tip.html.twig) Place two configuration files in the `config/packages` folder: - [`views.yaml`](https://github.com/ibexa/documentation-developer/blob/6.0/code_samples/tutorials/page_tutorial_starting_point/config/packages/views.yaml) - [`image_variations.yaml`](https://github.com/ibexa/documentation-developer/blob/6.0/code_samples/tutorials/page_tutorial_starting_point/config/packages/image_variations.yaml) In the `assets` folder in the project root: - in the `css` folder add the following stylesheet: [`style.css`](https://github.com/ibexa/documentation-developer/blob/6.0/code_samples/tutorials/page_tutorial_starting_point/assets/css/style.css) to it - add the [`header.jpg`](https://github.com/ibexa/documentation-developer/blob/6.0/code_samples/tutorials/page_tutorial_starting_point/assets/images/header.jpg) file to the `assets/images` folder In the `webpack.config.js` file in the project root folder, add the following line after `Encore.addEntry('app', './assets/app.js');`: ```js Encore.addStyleEntry('tutorial', [path.resolve(__dirname, './assets/css/style.css')]); ``` Next, in the terminal run the commands: ```bash yarn encore php bin/console cache:clear ``` > **Tip: Tip** > > Compiling assets with Webpack Encore is explained in [the beginner tutorial](https://doc.ibexa.co/en/saas/tutorials/beginner_tutorial/3_customize_the_front_page/#configuring-webpack). In the `src` folder create a `QueryType` subfolder and add [`MenuQueryType.php`](https://github.com/ibexa/documentation-developer/blob/6.0/code_samples/tutorials/page_tutorial_starting_point/src/QueryType/MenuQueryType.php) to it. This file takes care of displaying the top menu (for more information, see [the documentation](https://doc.ibexa.co/en/saas/templating/queries_and_controllers/content_queries/#query-types)). The structure of the new and modified files should look like: ![File structure](https://doc.ibexa.co/en/saas/tutorials/page_and_form_tutorial/img/enterprise_tut_file_structure.png) ## Create content Now return to the back office and create some content for your website. First, you can hide unneeded content items from the project root. Go to **Content structure** and select "Ibexa Digital Experience Platform". In the **Sub-items** tab, select all the current sub-items and click the **Hide Location** icon: ![Hiding content items you don't need](https://doc.ibexa.co/en/saas/tutorials/page_and_form_tutorial/img/enterprise_tut_hide_content.png) Next, under "Ibexa Digital Experience Platform", create three Folders. Call them *All Articles*, *Dog Breed Catalog* and *All Tips*. Remember that you can **Save and close** them, but you should use the **Publish** button. Next, create a few content items of proper content types in each of these folders: - 4 Articles (at least, to best see the effects of the Content Scheduler block that you can create in step 3.) - 3 Dog Breeds - 3 Tips ### Add images When you need an image, you can use one from [this image pack](https://doc.ibexa.co/en/saas/tutorials/page_and_form_tutorial/img/photos.zip). This lets you compare effects of your work to screenshots in the tutorial. At this point you're ready to proceed with the next step. # Step 2 — Prepare the Page > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). Learn how to build a Page with a custom layout. Editions: Experience In this step you can prepare and configure your front page, together with its layout and templates. ## Create Page layout Go to the front page of your website (``). You can see that it looks unfinished. However, you can still use the menu and look around the existing content in the website. ![It's a Dog's World - Starting point](https://doc.ibexa.co/en/saas/tutorials/page_and_form_tutorial/img/enterprise_tut_starting_point.png "It's a Dog's World - Starting point") > **Tip: Tip** > > At any point in the tutorial if you don't see the results of your last actions, try clearing the cache and regenerating assets: > > `php bin/console cache:clear` > > `yarn encore ` Log in to the back office. Go to **Content Structure**. The **Ibexa Digital Experience Platform** content item is the first page that is shown to the visitor. Here you can check what content type it belongs to: it's a *Landing page*. ![Ibexa Digital Experience Platform is a landing page](https://doc.ibexa.co/en/saas/tutorials/page_and_form_tutorial/img/enterprise_tut_home_is_an_lp.png) The page is displayed without any template. Click **Edit** to enter a mode that enables you to work with pages. You can see that the home page has only one drop zone. ![Empty Page with default layout](https://doc.ibexa.co/en/saas/tutorials/page_and_form_tutorial/img/enterprise_tut_empty_single_block.png) Click the **Fields** button on the left of the top bar to switch to editing page fields. Change the Title of the page to "Home". Then, publish the page to update its name. The design for the website you're making needs a layout with two zones: a main column and a narrower sidebar. Ibexa Experience provides only a one-zone default layout, so you need to create a new one. Preparing a new layout requires three things: - entry in configuration - thumbnail - template ### Add entry in configuration First create a new file for layout configuration, `config/packages/ibexa_fieldtype_page.yaml`: ```yaml ibexa_fieldtype_page: layouts: sidebar: identifier: sidebar name: Right sidebar description: Main section with sidebar on the right thumbnail: /assets/images/layouts/sidebar.png template: layouts/sidebar.html.twig zones: first: name: First zone second: name: Second zone ``` ### Add thumbnail > **Tip: Tip** > > For a detailed description of creating a Page layout, see [Page layouts](https://doc.ibexa.co/en/saas/templating/render_content/render_page/#render-a-layout). The `sidebar` (line 3) is the internal key of the layout. `name` (line 5) is displayed in the interface when the user selects a layout. The `thumbnail` (line 7) points to an image file that is shown when creating a new landing page next to the name. Use the [supplied thumbnail file](https://github.com/ibexa/documentation-developer/blob/6.0/code_samples/tutorials/page_tutorial_starting_point/public/assets/images/layouts/sidebar.png) and place it in the `public/assets/images/layouts/` folder. The `template` (line 8) points to the Twig file containing the template for this layout. ### Create page template Configuration points to `sidebar.html.twig` as the template for the layout. The template defines what zones are available in the layout. Create a `templates/layouts/sidebar.html.twig` file: ```html+twig
{% if zones[0].blocks %} {% set locationId = parameters.location is not null ? parameters.location.id : contentInfo.mainLocationId %} {% for block in zones[0].blocks %}
{{ render_esi(controller('Ibexa\\Bundle\\FieldTypePage\\Controller\\BlockController::renderAction', { 'locationId': locationId, 'contentId': contentInfo.id, 'blockId': block.id, 'versionNo': versionInfo.versionNo, 'languageCode': field.languageCode })) }}
{% endfor %} {% endif %}
``` The above template creates two columns and defines their widths. Each column is at the same time a zone, and each zone renders the blocks that it contains. > **Tip: Tip** > > In sites with multiple layouts you can separate the rendering of zones into a separate `zone.html.twig` template to avoid repeating the same code in every layout. > **Note: Note** > > A zone in a layout template **must have** the `data-ibexa-zone-id` attribute (lines 2 and 19). A block **must have** the `data-ibexa-block-id` attribute (lines 7 and 24). With these three elements: configuration, icon and template, the new layout is ready to use. ### Change Home Page layout Now you can change the Home Page to use the new layout. Edit Home and in the top bar select **Switch layout**. Choose the new layout called "Main section with sidebar on the right". The empty zones you defined in the template are visible in the editor. ![Select layout window](https://doc.ibexa.co/en/saas/tutorials/page_and_form_tutorial/img/enterprise_tut_select_layout.png) > **Tip: Tip** > > If the new layout isn't available when editing the page, you may need to clear the cache (using `php bin/console cache:clear`) and/or reload the app. ![Empty page with new layout](https://doc.ibexa.co/en/saas/tutorials/page_and_form_tutorial/img/enterprise_tut_new_layout.png) Publish the Home page. You can notice that it still has some additional text information. This is because the looks of a page are controlled by two separate template files, and you have only prepared one of those. The `sidebar.html.twig` file defines how zones are organized and how content is displayed in them. But you also need a general template file that is used for every page, regardless of its layout. Add this new template, `templates/full/landing_page.html.twig`: ```html+twig {% extends 'pagelayout.html.twig' %} {% block content %}
{{ ibexa_render_field(content, 'page') }}
{% endblock %} ``` This template renders the page content. If there is any additional content or formatting you would like to apply to every page, it should be placed in this template. Now you need to tell the app to use this template to render pages. Edit the `config/packages/views.yaml` file and add the following code under the `full:` key: ```yaml landing_page: template: full/landing_page.html.twig match: Identifier\ContentType: landing_page ``` After adding this template you can check the new page. The part between menu and footer should be empty, because you haven't added any content to it yet. ![Empty Page](https://doc.ibexa.co/en/saas/tutorials/page_and_form_tutorial/img/enterprise_tut_empty_page.png) # Step 3 — Use existing blocks > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). Learn how to use and customize built-in Page blocks. Editions: Experience In this step you can add a Content List block and a Content Scheduler block and customize them. ## Add a Content List block First, create an override template for the Content List block: `templates/blocks/contentlist/default.html.twig`: ```html+twig

{{ parentName }}

{% if contentArray|length > 0 %}
{% for content in contentArray %}
{{ ibexa_render_field(content.content, 'photo', { 'parameters': { 'alias': 'content_list' } }) }}

{{ ibexa_content_name(content.content) }}

{% if not ibexa_field_is_empty(content.content, 'short_description') %}
{{ ibexa_render_field(content.content, 'short_description') }}
{% endif %}
{% endfor %}
{% endif %}
``` Then add a configuration that tells the app to use this template instead of the default one. In `config/packages/ibexa_fieldtype_page.yaml` add the following code at the end of the file, under the `ibexa_fieldtype_page` key on the same level as `layouts`: ```yaml blocks: contentlist: views: default: template: blocks/contentlist/default.html.twig name: Content List ``` The template makes use of an [image variation](https://doc.ibexa.co/en/saas/content_management/images/images/index.md) (line 10). It's the thumbnail of the Dog Breed image that is displayed in the block. To configure this variation, open the `config/packages/image_variations.yaml` file and add the following code under the `image_variations` key: ```yaml content_list: reference: null filters: - { name: geometry/scaleheightdownonly, params: [ 81 ] } - { name: geometry/crop, params: [ 80, 80, 0, 0 ] } ``` Finally, add some styling to the block. Add the following CSS to the end of the `assets/css/style.css` file: ```css /* Landing Page */ @media only screen and (min-width: 992px) { aside > div { padding-left: 45px; } } /* Content list block */ .content-list-item { clear: left; min-height: 90px; padding-bottom: 5px; border-bottom: 1px solid black; } .content-list h5 { font-size: 1.3em; } .content-list-item-image { float: left; margin-right: 10px; } ``` Run `yarn encore ` to regenerate assets. At this point you can start adding blocks to the page. You do it in the page's Edit mode by dragging a block from the menu on the right to the correct zone on the page. Drag a *Content List* block from the menu to the left zone on the page. Click the block and fill in the form. Here you name the block and decide what it displays. Choose the *Dog Breed Catalog* folder as the Parent, select *Dog Breed* as the content type to be displayed, and choose a limit (3). This block will display the first three Dog Breeds from the database. ![Window with Content List options](https://doc.ibexa.co/en/saas/tutorials/page_and_form_tutorial/img/enterprise_tut_content_list_window.png) Click **Submit** and you should see a preview of what the block looks like with the dog breed information displayed. ![Content List Styled](https://doc.ibexa.co/en/saas/tutorials/page_and_form_tutorial/img/enterprise_tut_content_list_styled.png "Content List Styled") The block is displayed using the new template. Built-in blocks have default templates included in a clean installation, but you can override them. Publish the page now and move on to creating another type of block. ## Create a Content Scheduler block for featured articles The next block is the Content Scheduler block that airs articles at predetermined times. First, add a configuration that points to the layout. Go to `config/packages/ibexa_fieldtype_page.yaml` again and add the following code under `blocks` on the same level as the `contentlist` key: ```yaml schedule: views: featured: template: blocks/schedule/featured.html.twig name: Featured Schedule Block ``` The configuration defines one view for the Schedule block called `featured` and points to a `featured.html.twig` template. Create the new file `templates/blocks/schedule/featured.html.twig`: ```html+twig {% apply spaceless %}
{% endapply %} ``` When you look at the template, you can see three blocks, each of which render the content items using the `featured` view (line 11). So far you only have templates for `full` view for Articles. This means you need to create a `featured` view template, otherwise you get an error when trying to add content to the block. You need to modify the `config/packages/views.yaml` file to indicate when to use the template. Add the following code to this file, on the same level as the `full` key: ```yaml featured: article: template: featured/article.html.twig match: Identifier\ContentType: article ``` Now create a `templates/featured/article.html.twig` file: ```html+twig {% set imageAlias = ibexa_image_alias(content.getField('image'), content.versionInfo, 'featured_article') %}

{{ ibexa_content_name(content) }}

``` Like in the case of the Content List block, the template specifies an image variation. Add it in `config/packages/image_variations.yaml` under the `image_variations` key: ```yaml featured_article: reference: null filters: - { name: geometry/scaleheightdownonly, params: [ 200 ] } ``` The Block is already operational, but first update the stylesheet. Add the following CSS at the end of the `assets/css/style.css` file: ```css /* Featured articles Content Scheduler block */ .featured-article-container { background-size: cover; padding: 0; margin-bottom: 20px; } .featured-article { height: 200px; padding: 0; background-repeat: no-repeat; } .featured-article-link:link, .featured-article-link:visited { position: absolute; bottom: 0; margin-bottom: 0; background-color: rgba(255,255,255,.8); color: #000; font-size: 1.1em; padding: 7px; } .featured-article-link:hover, .featured-article-link:focus { color: #654d31; text-decoration: none; border-bottom: none; } ``` Run `yarn encore ` to regenerate assets. At this point you can add a new Content Scheduler block to your page and fill it with content to see how it works. > **Tip: Tip** > > If you don't see the featured block template, you may need to clear the cache (using `php bin/console cache:clear`) and/or reload the app. Go back to editing the Home page and drag a *Content Scheduler* block from the pane on the right to the main zone in the layout, above the *Content List* block. Select the block and click the **Block Settings** icon. Set the *Limit* to 3 and click **Select Content**. Navigate to the "All Articles" folder and select the articles you had created and confirm. ![Selecting Articles for the Schedule Block](https://doc.ibexa.co/en/saas/tutorials/page_and_form_tutorial/img/enterprise_tut_select_articles.png) Accept the suggested airtime and click **Submit**. Now click the Airtime button next to one of the Articles and choose a time in the future. This article is listed in the queue. ![Content Scheduler with scheduled content](https://doc.ibexa.co/en/saas/tutorials/page_and_form_tutorial/img/enterprise_tut_choosing_airtime.png) Publish the page. Return to the editing page. Click the **Schedule** button on the left of the top bar, click the **Show timeline** button, and close. You can now see a slider at the top of the page. You can move it to different times and preview what the *Content Scheduler* block looks like at different hours. Content is shown when you move the slider to the point when it airs. > **Tip: Tip** > > At this point you have configured the Content Scheduler block to work with Articles only. If you try to add Content of any other type, you can see an error. This is because there is no `featured` view for content other than Articles defined at the moment. ![Front page after adding Featured Block](https://doc.ibexa.co/en/saas/tutorials/page_and_form_tutorial/img/enterprise_tut_page_with_featured_articles.png "Front page after adding Featured Block") # Step 4 — Create a custom block > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). Try creating a custom page block with specific logic. Editions: Experience This step guides you through creating a custom block. The custom block displays a randomly chosen content item from a selected folder. To create a custom block from scratch you need four elements: - block configuration - a template - a listener - the listener registered as a service ## Block configuration In `config/packages/ibexa_fieldtype_page.yaml` add the following block under the `blocks` key: ```yaml random: name: Random block thumbnail: /assets/images/blocks/random_block.svg#random views: random: template: blocks/random/default.html.twig name: Random Content Block View attributes: parent: type: embed name: Parent validators: not_blank: message: You must provide value regexp: options: pattern: '/[0-9]+/' message: Choose a content item ``` This configuration defines one attribute, `parent`. Use it to select the folder containing tips. ## Block template You also need to create the block template, `templates/blocks/random/default.html.twig`: ```html+twig

{{ 'Tip of the Day'|trans }}

{{ ibexa_content_name(randomContent) }}
{{ ibexa_render_field(randomContent, 'body') }}
``` ## Block listener Block listener provides the logic for the block. It's contained in `src/Event/RandomBlockListener.php`: ```php 'onBlockPreRender', ]; } public function onBlockPreRender(PreRenderEvent $event): void { $blockValue = $event->getBlockValue(); /** @var \Ibexa\FieldTypePage\FieldType\Page\Block\Renderer\Twig\TwigRenderRequest $renderRequest */ $renderRequest = $event->getRenderRequest(); $parameters = $renderRequest->getParameters(); $contentIdAttribute = $blockValue->getAttribute('parent'); $location = $this->loadLocationByContentId((int) $contentIdAttribute->getValue()); $contents = $this->findContentItems($location); shuffle($contents); $parameters['randomContent'] = reset($contents); $renderRequest->setParameters($parameters); } private function findContentItems(Location $location): array { $query = new Query(); $query->query = new Criterion\LogicalAnd( [ new Criterion\ParentLocationId($location->id), new Criterion\Visibility(Criterion\Visibility::VISIBLE), ] ); $searchHits = $this->searchService->findContent($query)->searchHits; $contentArray = []; foreach ($searchHits as $searchHit) { $contentArray[] = $searchHit->valueObject; } return $contentArray; } private function loadLocationByContentId(int $contentId): Location { $contentInfo = $this->contentService->loadContentInfo($contentId); return $this->locationService->loadLocation($contentInfo->mainLocationId); } } ``` At this point the new custom block is ready to be used. You're left with the last cosmetic changes. First, the new Block has a broken icon in the **Page blocks** toolbox in page mode. This is because you haven't provided this icon yet. If you look back to the YAML configuration, you can see the icon file defined as `random_block.svg` (line 4). Download [the provided file](https://github.com/ibexa/documentation-developer/blob/6.0/code_samples/tutorials/page_tutorial_starting_point/public/assets/images/blocks/random_block.svg) and place it in `public/assets/images/blocks`. Finally, add some styling for the new block. Add the following to the end of the `assets/css/style.css` file: ```css /* Random block */ .random-block { border: 1px solid #83705a; border-radius: 5px; padding: 0 25px 25px 25px; margin-top: 15px; } .random-block h4 { font-variant: small-caps; font-size: 1.2em; } .random-block h5 { font-size: 1.2em; } .random-block-text { font-size: .85em; } ``` Run `yarn encore ` to regenerate assets. Go back to editing the front page. Drag a Random Block from the **Page blocks** toolbox on the right to the page's side column. Access the block's settings and choose the "All Tips" folder from the menu. Save and publish all the changes. Refresh the home page. The Tip of the Day block displays a random Tip from the "Tips" folder. Refresh the page a few more times and you can see the tip change randomly. ![Random Block with a Tip](https://doc.ibexa.co/en/saas/tutorials/page_and_form_tutorial/img/enterprise_tut_random_block.png "Random Block with a Tip") To learn more about custom Page Builder blocks, see [Create custom page block](https://doc.ibexa.co/en/saas/content_management/pages/create_custom_page_block/index.md). # Step 5 — Create a newsletter form > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). Learn how to create a sign-up form and how to view and manage its submissions. Editions: Experience The final step of this tutorial assists you in adding to the home page a Form block for signing up to a newsletter. > **Caution: Known limitation** > > To have multiple instances of the same form on one page, create several identical form blocks. Otherwise, you may encounter issues with submitting data from all forms at the same time. ## Add a Form block Start with creating a Form content item. In the main menu, go to **Content** -> **Forms**, click **Create content** and select **Form**. Provide the title, for example, "Sign up for Newsletter" and click **Build form**. In the Form Builder, add and configure (using the **Basic** and **Validation** tabs) the following form fields: | Form field | Name | Required | Additional properties | | ----------------- | ------------- | -------- | ----------------------------------------------------- | | Single line input | Name | yes | Minimum length = 3 | | Single line input | Surname | no | Minimum length = 3 | | Dropdown | Select topic | yes | Options: - News - Tips - Articles | | Email | Email address | yes | — | | Captcha | CAPTCHA | — | — | | Button | Sign up! | — | Action: Show a message Message to display: Thank you! | The configuration should look like this: ![Adding fields to Newsletter Form](https://doc.ibexa.co/en/saas/tutorials/page_and_form_tutorial/img/enterprise_tut_form_creation.png "Adding fields to Newsletter Form") When you add all the fields, save the form and click **Publish**. Now you can edit the front page and add a Form block below the Random block. Edit the block and select the form you created. Click **Submit**. The Page should refresh with the Form block. ![Newsletter Form Block](https://doc.ibexa.co/en/saas/tutorials/page_and_form_tutorial/img/enterprise_tut_first_form.png "Raw Newsletter Form Block") It clearly differs from the page design, so you also need to customize the block's layout. ## Change the block template First, add a new template for the Form block to align it with the Random block design. Create a `newsletter.html.twig` file in `templates/blocks/form/`: ```html+twig
{{ ibexa_http_cache_tag_relation_location_ids(locationId) }} {{ render(controller('ibexa_content::viewAction', { 'contentId': contentId, 'locationId': locationId, 'viewType': 'embed' })) }}
``` This template extends the default block layout by adding an additional class (line 1) that shares CSS styling with the Random block. Append the new template to the block by adding it to `config/packages/ibexa_fieldtype_page.yaml`. Add the following configuration under the `blocks` key at the same level as other block names, for example, `random`: ```yaml form: views: default: template: blocks/form/newsletter.html.twig name: Newsletter Form View ``` Now you have to apply the template to the block. Go back to editing the page. Edit the Form block again. In the **Design** tab, select the **Newsletter Form View** and click **Submit**. The block remains unchanged, but the results are visible when you add CSS styling. ## Change the field template At this point, you need to change the field template. This results in alternating the position and design of the Form fields. Create a `form_field.html.twig` file in `templates/fields/`: ```html+twig {% block ibexa_form_field %} {% set formValue = field.value.getForm() %} {% if formValue %} {% set form = formValue.createView() %} {% form_theme form 'bootstrap_4_layout.html.twig' %} {% apply spaceless %} {% if not ibexa_field_is_empty(content, field) %} {{ form(form) }} {% endif %} {% endapply %} {% endif %} {% endblock %} ``` Next, assign the template to the page. In `config/packages/views.yaml`, at the same level as `page_layout`, add: ```yaml field_templates: - { template: fields/form_field.html.twig, priority: 30 } ``` Clear the cache by running `bin/console cache:clear` and refresh the page to see the results. ## Configure the Form field Before applying the final styling of the block, you need to configure the [CAPTCHA field](https://doc.ibexa.co/en/saas/content_management/forms/work_with_forms/#captcha-field). In `config/packages`, add a `gregwar_captcha.yaml` file with the following configuration: ```yaml gregwar_captcha: width: 150 invalid_message: Please, enter again. length: 4 ``` The configuration resizes the CAPTCHA image (line 2), changes the error message (line 3), and shortens the authentication code (line 4). ## Add stylesheet The remaining step in configuring the block is adding CSS styling. Add the following code to `assets/css/style.css`: ```css /* Newsletter Form block */ .block-form { border: 1px solid #8b7f7b; border-radius: 5px; padding: 0 25px 25px 25px; margin-top: 15px; } .block-form .ibexa_string-field { display: inline-block; font-variant: small-caps; font-size: 1.2em; width: 100%; text-align: right; padding-top: 20px; } .block-form .col-form-label { display: none; } .block-form .form-group { font-variant: small-caps; font-size: 1em; margin-top: 12px; } .captcha_image { padding-left: 10px; padding-bottom: 10px; } .captcha_reload { font-variant: all-small-caps; padding-left: 5px; } .btn-primary { background-color: rgb(103,85,80); border-color: rgb(103,85,80); margin-top: 15px; margin-bottom: -20px; } .block-form h3 { font-variant: all-small-caps; text-align: center; } ``` Reinstall the assets and clear the cache by running the following commands: ```bash yarn encore php bin/console cache:clear ``` Your newsletter form block is ready. ![Newsletter Form Block](https://doc.ibexa.co/en/saas/tutorials/page_and_form_tutorial/img/enterprise_tut_final_form.png "Newsletter Form Block") Refresh the page and enter a couple of mock submissions. ## Manage the submissions You can view all submissions in the back office. Go to **Forms** page. From the content tree, select the Form and click the **Submissions** tab. There, after selecting submission(s), click **Download submissions** or **Delete submission**. To see details about a submission, click the view icon. ![Collect Form Submissions](https://doc.ibexa.co/en/saas/tutorials/page_and_form_tutorial/img/enterprise_tut_form_collect_sub.png "Collect Form Submissions") For more information, see [viewing form results](https://doc.ibexa.co/projects/userguide/en/6.0/content_management/work_with_forms/#view-results). ## Congratulations! You have finished the tutorial and created your first customized page. You have learned how to: - Create and customize a Page - Make use of existing blocks and adapt them to your needs - Plan content airtime using the Content Scheduler block - Create custom blocks - Use Form Builder and configure your form - Apply custom styling to blocks ![Final result of the tutorial](https://doc.ibexa.co/en/saas/tutorials/page_and_form_tutorial/img/enterprise_tut_main_screen.png "Final result of the tutorial") # Creating a Point 2D field type > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). Go through a field type tutorial to learn how to create a custom field type based on the built-in Generic field type. This tutorial covers the creation and development of a custom Cohesivo [field type](https://doc.ibexa.co/en/saas/content_management/field_types/create_custom_generic_field_type/index.md). The Generic field type is a powerful extension point. It enables you to build complex solutions on a ready-to-go field type template. Field types are responsible for: - Storing data, either using the native storage engine mechanisms or specific means - Validating input data - Making the data searchable (if applicable) - Displaying fields For more information, see [field type documentation](https://doc.ibexa.co/en/saas/content_management/field_types/field_types/index.md). It describes how each component of a field type interacts with the various layers of the system, and how to implement them. ## Intended audience This tutorial is aimed at developers who are familiar with Cohesivo and are comfortable with operating in PHP and Symfony. ## Content of the tutorial This tutorial shows you how to use the Generic field type as a template for a custom field type. You: - create a custom Point 2D field type with two coordinates as input, for example '4,5' - register the new field type as a service and define its template - add basic validation to your Point 2D - add data migration to the field type so you're able to change its output ## Steps In this tutorial you go through the following steps: - [1. Implement the Point 2D Value class](https://doc.ibexa.co/en/saas/tutorials/generic_field_type/1_implement_the_point2d_value_class/index.md) - [2. Define the Point 2D field type](https://doc.ibexa.co/en/saas/tutorials/generic_field_type/2_define_point2d_field_type/index.md) - [3. Create form for editing field type](https://doc.ibexa.co/en/saas/tutorials/generic_field_type/3_create_form_for_point2d/index.md) - [4. Introduce a template](https://doc.ibexa.co/en/saas/tutorials/generic_field_type/4_introduce_a_template/index.md) - [5. Add a new Point 2D field](https://doc.ibexa.co/en/saas/tutorials/generic_field_type/5_add_a_field/index.md) - [6. Implement Point 2D settings](https://doc.ibexa.co/en/saas/tutorials/generic_field_type/6_settings/index.md) - [7. Add basic validation](https://doc.ibexa.co/en/saas/tutorials/generic_field_type/7_add_a_validation/index.md) - [8. Data migration between field type versions](https://doc.ibexa.co/en/saas/tutorials/generic_field_type/8_data_migration/index.md) # Step 1 - Implement the Point 2D Value class > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). Learn how to create a Value class that stores the value of the field. ## Project installation To start the tutorial, you need a clean Cohesivo installation running in the `dev` environment. Open your project with a clean installation and create the base directory for a new Point 2D field type in `src/FieldType/Point2D`. ## The Value class The Value class of a field type is by design very simple. It's used to represent an instance of the field type within a content item. Each field presents its data using an instance of the Type's Value class. For more information about field type Value, see [Value handling](https://doc.ibexa.co/en/saas/content_management/field_types/type_and_value/#value-handling). > **Tip: Tip** > > According to the convention, the class representing field type Value should be named `Value` and should be placed in the same namespace as the Type definition. > **Caution: Simple hash values** > > A simple hash value always means an array of scalar values and/or nested arrays of scalar values. To avoid issues with format conversion, don't use objects inside the simple hash values. The Point 2D Value class contains: - private properties, used to store the actual data - an implementation of the `__toString()` method, required by the Value interface By default, the constructor from `FieldType\Value` is used. The Point 2D is going to store two elements (coordinates for point 2D): - `x` value - `y` value At this point, it doesn't matter where they're stored. You want to focus on what the field type exposes as an API. `src/FieldType/Point2D/Value.php` should have the following properties: ```php public function __construct( private ?float $x = null, private ?float $y = null ) { } ``` A Value class must also implement the `Ibexa\Contracts\Core\FieldType\Value` interface. To match the `FieldType\Value` interface, you need to implement `__toString()` method. You also need to add getters and setters for `x` and `y` properties. This class represents the point 2D. The final code should look like this: ```php x; } public function setX(?float $x): void { $this->x = $x; } public function getY(): ?float { return $this->y; } public function setY(?float $y): void { $this->y = $y; } public function __toString(): string { return "({$this->x}, {$this->y})"; } } ``` # Step 2 - Define the Point 2D field type > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). Learn how to create the Type class which contains the logic for the field. ## The Type class The Type contains logic of the field type: validating data, transforming from various formats, describing the validators, and more. In this example Point 2D field type extends the `Ibexa\Contracts\Core\FieldType\Generic\Type` class. For more information about the Type class of a field type, see [Type class](https://doc.ibexa.co/en/saas/content_management/field_types/type_and_value/#type-class). ## Field type identifier First, create `src/FieldType/Point2D/Type.php`. Add a `getFieldTypeIdentifier()` method to it. The new method returns the string that **uniquely** identifies your field type, in this case `point2d`: ```php For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). Learn how to create a form used for editing a custom field definition. ## Create a form To edit your new field type, create a `Point2DType.php` form in the `src/Form/Type` directory. Next, add a `Point2DType` class that extends the `AbstractType` and implements the `buildForm()` method. This method adds fields for `x` and `y` coordinates. ```php add('x', NumberType::class); $builder->add('y', NumberType::class); } } ``` ## Add a Form Mapper Interface The FormMapper adds the field definitions into Symfony forms using the `add()` method. The `FieldValueFormMapperInterface` provides an edit form for your field type in the administration interface. For more information about the FormMappers, see [field type form and template](https://doc.ibexa.co/en/saas/content_management/field_types/form_and_template/index.md). First, implement a `FieldValueFormMapperInterface` interface (`Ibexa\Contracts\ContentForms\FieldType\FieldValueFormMapperInterface`) to field type definition in the `src/FieldType/Point2D/Type.php`. Next, implement a `mapFieldValueForm()` method and invoke `FormInterface::add` method with the following arguments (highlighted lines): - Name of the property the field value maps to: `value` - Type of the field: `Point2DType::class` - Custom options: `required` and `label` Final version of the Type class should have the following statements and functions: ```php getFieldDefinition(); $fieldForm->add('value', Point2DType::class, [ 'required' => $definition->isRequired, 'label' => $definition->getName(), ]); } } ``` Finally, add a `configureOptions` method and set default value of `data_class` to `Value::class` in `src/Form/Type/Point2DType.php`. It allows your form to work on this object. ```php add('x', NumberType::class); $builder->add('y', NumberType::class); } public function configureOptions(OptionsResolver $resolver): void { $resolver->setDefaults([ 'data_class' => Value::class, ]); } } ``` ## Add a new tag Next, add the `ibexa.admin_ui.field_type.form.mapper.value` tag to `config/services.yaml`: ```yaml App\FieldType\Point2D\Type: tags: - { name: ibexa.field_type, alias: point2d } - { name: ibexa.admin_ui.field_type.form.mapper.value, fieldType: point2d } ``` # Step 4 - Introduce a template > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). Learn how to add a template for rendering the custom field on the site front. ## Point 2D template To display data from the field type, you need to create and register a template for it. Each field type template receives a set of variables that can be used to achieve the desired goal. In this case the most important variable is the `field`, an instance of `Ibexa\Contracts\Core\Repository\Values\Content\Field`. In addition to its own metadata (for example, `id` or `fieldDefIdentifier`), it exposes the field Value through the `value` property. Remember that field type templates can be overridden to tweak what is displayed and how. For more information, see [field type templates](https://doc.ibexa.co/en/saas/content_management/field_types/form_and_template/#content-view-templates). First, create a `point2d_field.html.twig` template in the `templates` directory. It defines the default display of a Point 2D. Your basic template for Point 2D should look like this: ```html+twig {% block point2d_field %} ({{ field.value.getX() }}, {{ field.value.getY() }}) {% endblock %} ``` ## Template mapping Next, provide the template mapping in `config/packages/ibexa.yaml`: ```yaml ibexa: system: default: field_templates: - { template: 'point2d_field.html.twig', priority: 0 } ``` # Step 5 - Add a new Point 2D field > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). Learn how to use your custom field type by adding a field to a content type and creating an instance. All actions in this step are done in the admin interface also called the back office. Go to the admin interface (`/admin`) and log in with the default username: `admin` using the password specified during installation. ## Add new content type In the back office, the main menu, go to the **Content types** page. Under **Content** category, create a new content type: ![Creating new content type](https://doc.ibexa.co/en/saas/tutorials/generic_field_type/img/create_new_content_type.png) New content type should have the following settings: - **Name:** Point 2D - **Identifier:** point_2d - **Fields:** point2d.name ![Adding new field](https://doc.ibexa.co/en/saas/tutorials/generic_field_type/img/point2d_field_definition.png) Next, define **point2d** with the following fields: | Field type | Name | Identifier | Required | Translatable | | ---------- | -------- | ---------- | -------- | ------------ | | point2d | Point 2D | `point_2d` | yes | no | ![Defining Point 2D](https://doc.ibexa.co/en/saas/tutorials/generic_field_type/img/new_field_definition.png) Save everything and go back to the **Content/Content structure** tab. ## Create your content In **Content structure**, select **Create content**. There, under **Content**, you should see Point 2D content type you added. Click it to create new content. ![Selecting Point 2D from sidebar](https://doc.ibexa.co/en/saas/tutorials/generic_field_type/img/menu_point2d.png) Here, you can fill in coordinates of your point, for example, 3, 5. Provided coordinates are used as a title for a new point. ![Creating Point 2D](https://doc.ibexa.co/en/saas/tutorials/generic_field_type/img/creating_new_point2d.png) Click **Publish**. Now, you should see a new **(3,5)** point in the content tree. > **Tip: Tip** > > If you cannot see the results or encounter an error, clear the cache and reload the application. ![New Point 2D](https://doc.ibexa.co/en/saas/tutorials/generic_field_type/img/new_point2d.png) # Step 6 - Implement Point 2D settings > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). Learn how to add settings that format the field value. Implementing settings enables you to define the format for displaying the field on the page. To do so, create the `format` field where you're able to change the way coordinates for Point 2D are displayed. ## Define field type format In this step you create the `format` field for Point 2D coordinates. To do that, you need to define a `SettingsSchema` definition. You also specify coordinates as placeholder values `%x%` and `%y%`. Open `src/FieldType/Point2D/Type.php` and add a `getSettingsSchema` method according to the following code block: ```php [ 'type' => 'string', 'default' => '(%x%, %y%)', ], ]; } public function mapFieldValueForm(FormInterface $fieldForm, FieldData $data): void { $definition = $data->getFieldDefinition(); $fieldForm->add('value', Point2DType::class, [ 'required' => $definition->isRequired, 'label' => $definition->getName(), ]); } } ``` ## Add a format field In this part you define and implement the edit form for your field type. Define a `Point2DSettingsType` class and add a `format` field in `src/Form/Type/Point2DSettingsType.php`: ```php add('format', TextType::class); } } ``` ## FieldDefinitionFormMapper Interface Now, enable the user to add the coordinates which are validated. In `src/FieldType/Point2D/Type.php` you: - implement the `FieldDefinitionFormMapperInterface` interface - add a `mapFieldDefinitionForm` method at the end that defines the field settings ```php add('fieldSettings', Point2DSettingsType::class, [ 'label' => false, ]); } } ``` Complete Type.php code ```php [ 'type' => 'string', 'default' => '(%x%, %y%)', ], ]; } public function mapFieldValueForm(FormInterface $fieldForm, FieldData $data): void { $definition = $data->getFieldDefinition(); $fieldForm->add('value', Point2DType::class, [ 'required' => $definition->isRequired, 'label' => $definition->getName(), ]); } public function mapFieldDefinitionForm(FormInterface $fieldDefinitionForm, FieldDefinitionData $data): void { $fieldDefinitionForm->add('fieldSettings', Point2DSettingsType::class, [ 'label' => false, ]); } } ``` ## Add a new tag Next, add `FieldDefinitionFormMapper` as an extra tag definition for `App\FieldType\Point2D\Type` in `config/services.yaml`: ```yaml App\FieldType\Point2D\Type: tags: - { name: ibexa.field_type, alias: point2d } - { name: ibexa.admin_ui.field_type.form.mapper.value, fieldType: point2d } - { name: ibexa.admin_ui.field_type.form.mapper.definition, fieldType: point2d } ``` ## Field type definition To be able to display the new `format` field, you need to add a template for it. Create `templates/point2d_field_type_definition.html.twig`: ```html+twig {% block point2d_field_definition_edit %}
{{- form_label(form.fieldSettings.format) -}} {{- form_errors(form.fieldSettings.format) -}} {{- form_widget(form.fieldSettings.format) -}}
{% endblock %} ``` ### Add configuration for the format field Next, provide the template mapping in `config/packages/ibexa.yaml`: ```yaml ibexa: system: default: field_templates: - { template: 'point2d_field.html.twig', priority: 0 } fielddefinition_edit_templates: - { template: 'point2d_field_type_definition.html.twig', priority: 0 } ``` ## Redefine template Finally, redefine the Point 2D template, so it accommodates the new `format` field. In `templates/point2d_field.html.twig` replace the content with: ```html+twig {% block point2d_field %} {{ fieldSettings.format|replace({ '%x%': field.value.x, '%y%': field.value.y }) }} {% endblock %} ``` ## Edit the content type Now, in the back office, you can go to **Content types** and see the results of your work by editing the Point 2D content type. > **Tip: Tip** > > If you cannot see the results or encounter an error, clear the cache and reload the application. Add new format `(%x%, %y%)` in the **Format** field as shown in the screen below. ![Point 2D definition with format field](https://doc.ibexa.co/en/saas/tutorials/generic_field_type/img/field_definition_format_field.png) # Step 7 - Add basic validation > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). Learn how to validate custom field data. To provide basic validation that ensures both coordinates are provided, add assertions to the `src/FieldType/Point2D/Value.php`: ```php x; } public function setX(?float $x): void { $this->x = $x; } public function getY(): ?float { return $this->y; } public function setY(?float $y): void { $this->y = $y; } public function __toString(): string { return "({$this->x}, {$this->y})"; } } ``` As a result, if a user tries to publish the Point 2D with one value, they receive an error message. ![Point 2D validation](https://doc.ibexa.co/en/saas/tutorials/generic_field_type/img/point2d_validation.png) # Step 8 - Data migration between field type versions > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). Learn how to serialize and deserialize field data to enable sorting or search. Adding data migration enables you to change the output of the field type to fit your current needs. This process is important when a field type needs to be compared for sorting and searching purposes. Serialization allows changing objects to array by normalizing them, and then to the selected format by encoding them. In reverse, deserialization changes different formats into arrays by decoding and then denormalizing them into objects. For more information on Serializer Component, see [Symfony documentation](https://symfony.com/doc/7.4/serializer.html). ## Normalization First, you need to add support for normalization in a `src/Serializer/Point2D/ValueNormalizer.php`: ```php */ public function normalize($object, ?string $format = null, array $context = []): array { return [ $object->getX(), $object->getY(), ]; } public function supportsNormalization(mixed $data, ?string $format = null, array $context = []): bool { return $data instanceof Value; } public function getSupportedTypes(?string $format): array { return [ Value::class => true, ]; } } ``` > **Note: Note** > > The `ValueDenormalizer` and `ValueNormalizer` service definitions are automatically registered by Symfony as services in `config/services.yaml`, without the need to manually define them. ## Backward compatibility To accept old versions of the field type you need to add support for denormalization in a `src/Serializer/Point2D/ValueDenormalizer.php`: ```php true, ]; } } ``` ## Change format on the fly To change the format on the fly, you need to replace the constructor and class properties in `src/FieldType/Point2D/Value.php`: ```php #[Assert\NotBlank] private ?float $x = null; #[Assert\NotBlank] private ?float $y = null; /** @param list $coords */ public function __construct(array $coords = []) { if (!empty($coords)) { $this->x = $coords[0]; $this->y = $coords[1]; } } ``` Now you can change the internal representation format of the Point 2D field type. # API # API > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). Cohesivo is an API-first product and provides APIs to handle content and repository information. Cohesivo is an API-first product and provides APIs to handle content and repository information. ## Web API - [REST API usage](https://ez-systems-developer-documentation--3381.com.readthedocs.build/en/3381/api/rest_api/rest_api_usage/rest_api_usage/): The REST API covers objects in the Cohesivo Repository with regular and custom HTTP methods, such as GET or PUBLISH, and HTTP headers. - [GraphQL](https://ez-systems-developer-documentation--3381.com.readthedocs.build/en/3381/api/graphql/graphql/): GraphQL enables making concise, readable requests to Cohesivo APIs. - [MCP Servers](https://ez-systems-developer-documentation--3381.com.readthedocs.build/en/3381/ai/mcp/mcp/): Overview of MCP resources in Cohesivo ## PHP API - [PHP API](https://ez-systems-developer-documentation--3381.com.readthedocs.build/en/3381/api/php_api/php_api/): Public PHP API exposes the Repository in a number of services and allows creating, reading, updating, managing, and deleting objects. - [Event reference](https://ez-systems-developer-documentation--3381.com.readthedocs.build/en/3381/api/event_reference/event_reference/): Cohesivo dispatches events before and after you perform different operations in the back office and on the Repository. # PHP API > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). Public PHP API exposes the Repository in a number of services and allows creating, reading, updating, managing, and deleting objects. The [public PHP API](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/index.md) enables you to interact with Cohesivo's Repository and content model from your PHP code. You can use it to create, read, update, manage, and delete all objects available in Cohesivo, namely content and related objects such as sections, locations, content types, or languages. The PHP API is built on top of a layered architecture, including a persistence SPI that abstracts storage. Using the API ensures that your code is forward compatible with future releases based on other storage engines. ## Using API services The API provides access to content, user, content types, and other features through various services. The full list of available services covers: - [BatchOrderService](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-Cart-BatchOrderServiceInterface.html) - [CorporateAccountService](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-CorporateAccount-Service-CorporateAccountService.html) (recommended for company creation) - [CompanyService](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-CorporateAccount-Service-CompanyService.html) - [ContentService](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-Core-Repository-ContentService.html) - [ContentTypeService](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-Core-Repository-ContentTypeService.html) - [FieldTypeService](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-Core-Repository-FieldTypeService.html) - [InvitationService](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-User-Invitation-InvitationService.html) - [LanguageService](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-Core-Repository-LanguageService.html) - [LocationService](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-Core-Repository-LocationService.html) - [MemberService](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-CorporateAccount-Service-MemberService.html) - [NotificationService](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-Core-Repository-NotificationService.html) - [ObjectStateService](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-Core-Repository-ObjectStateService.html) - [RoleService](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-Core-Repository-RoleService.html) - [SearchService](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-Core-Repository-SearchService.html) - [SectionService](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-Core-Repository-SectionService.html) - [ShippingAddressService](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-CorporateAccount-Service-ShippingAddressService.html) - [SpreadsheetProcessorInterface](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-Cart-FileProcessor-SpreadsheetProcessorInterface.html) - [TaxonomyService](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-Taxonomy-Service-TaxonomyServiceInterface.html) - [TranslationService](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-Core-Repository-TranslationService.html) - [TrashService](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-Core-Repository-TrashService.html) - [URLAliasService](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-Core-Repository-URLAliasService.html) - [URLService](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-Core-Repository-URLService.html) - [URLWildcardService](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-Core-Repository-URLWildcardService.html) - [UserPreferenceService](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-Core-Repository-UserPreferenceService.html) - [UserService](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-Core-Repository-UserService.html) You can access the PHP API by injecting relevant services into your code: - By using [auto-wiring](https://symfony.com/doc/7.4/service_container/autowiring.html), and the service class name in the `Ibexa\Contracts` namespace (see `bin/console debug:autowiring | grep Ibexa.Contracts`). - By using [service parameters](https://symfony.com/doc/7.4/service_container.html#service-container-parameters), and service aliases (see `bin/console debug:autowiring | grep ibexa.api`). - By using the repository's `get[ServiceName]()` methods, for example, [`Repository::getContentService()`](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-Core-Repository-Repository.html#method_getContentService), or [`getUserService()`](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-Core-Repository-Repository.html#method_getUserService). (Prefer injecting several Repository's dedicated services instead of the whole Repository if the Repository itself isn't needed.) > **Caution: Caution** > > The PHP API's services can be accessed with `Ibexa\Bundle\Core\Controller::getRepository()` by extending it from a [custom controller](https://doc.ibexa.co/en/saas/templating/queries_and_controllers/controllers/index.md), but such approach isn't recommended, and you should prefer dependency injection. ## Value objects The services provide interaction with read-only value objects from the [`Ibexa\Contracts\Core\Repository\Values`](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/namespaces/ibexa-contracts-core-repository-values.html) namespace. Those objects are divided into sub-namespaces, such as [`Content`](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/namespaces/ibexa-contracts-core-repository-values-content.html), [`User`](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/namespaces/ibexa-contracts-core-repository-values-user.html) or [`ObjectState`](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/namespaces/ibexa-contracts-core-repository-values-objectstate.html). Each sub-namespace contains a set of value objects, such as [`Content\Content`](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-Core-Repository-Values-Content-Content.html) or [`User\Role`](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-Core-Repository-Values-User-Role.html). Value objects come with their own properties, such as `$content->id` or `$location->hidden`, and with methods that provide access to more related information, such as [`Content\Relation::getSourceContentInfo()`](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-Core-Repository-Values-Content-Relation.html#method_getSourceContentInfo) or [`User\Role::getPolicies()`](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-Core-Repository-Values-User-Role.html#method_getPolicies). ### Creating and updating objects Value objects fetch data from the repository and are read-only. To create and modify repository values, use data structures, such as [`ContentService::newContentCreateStruct()`](https://github.com/ibexa/core/blob/v4.6.6/src/contracts/Repository/ContentService.php#L572) or [`LocationService::newLocationUpdateStruct()`](https://github.com/ibexa/core/blob/v4.6.6/src/contracts/Repository/LocationService.php#L238). ### Value info objects Some complex value objects have an `Info` counterpart, for example [`ContentInfo`](https://github.com/ibexa/core/blob/6.0/src/contracts/Repository/Values/Content/ContentInfo.php) for [`Content`](https://github.com/ibexa/core/blob/6.0/src/contracts/Repository/Values/Content/Content.php). These objects provide you with lower-level information. For instance, `ContentInfo` contains `currentVersionNo` or `remoteId`, while `Content` enables you to retrieve fields, content type, or previous versions. > **Note: Note** > > The public PHP API value objects should not be serialized. > > Serialization of value objects, for example, `Ibexa\Contracts\Core\Repository\Values\Content\ContentInfo` / `Ibexa\Contracts\Core\Repository\Values\Content\VersionInfo` or `Ibexa\Contracts\Core\Repository\Values\Content\Location` results in memory limit exceeded error. ## Authentication One of the responsibilities of the repository is user authentication. Every action is executed *as* a user. When you use the PHP API, authentication is performed in three ways: - [automatically in the back office](#back-office-authentication) - [by using `sudo()`](#using-sudo) - by [setting the Repository user](#setting-the-repository-user) ### Back office authentication When actions are performed through the back office, they're executed as the logged-in user. This user's permissions affects the behavior of the repository. The user may, for example, not be allowed to create content, or view a particular section. ### Using `sudo()` To skip permission checks, you can use the `sudo()` method. It allows API execution to be performed with full access, sand-boxed. You can use this method to perform an action that the current user doesn't have permissions for. For example, to [hide a Location](https://doc.ibexa.co/en/saas/content_management/content_api/managing_content/#hiding-and-revealing-locations), use: ```php use Ibexa\Contracts\Core\Repository\Repository; use Ibexa\Contracts\Core\Repository\Values\Content\Location; //... /** * @var Repository $repository * @var Location $location */ $hiddenLocation = $repository->sudo(static fn (Repository $repository): Location => $repository->getLocationService()->hideLocation($location)); ``` ### Setting the repository user In a command line script, the repository runs as if executed by the anonymous user. While [using `sudo()`](#using-sudo) is the recommended option, you can also set the current user to a user with necessary permissions to achieve the same effect. To identify as a different user, you need to use the `UserService` together with `PermissionResolver` (in the example `admin` is the login of the administrator user): ```php $user = $this->userService->loadUserByLogin('admin'); $this->permissionResolver->setCurrentUserReference($user); ``` > **Tip: Tip** > > [`Ibexa\Contracts\Core\Repository\PermissionService`](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-Core-Repository-PermissionService.html) can be injected to have a Service which provides both `PermissionResolver` and `PermissionCriterionResolver`. It supports auto-wiring. This isn't required in template functions or controller code, as the HTTP layer takes care of identifying the user, and automatically sets it in the repository. ## Exception handling PHP API uses [Exceptions](https://www.php.net/exceptions) to handle errors. Each API method may throw different exceptions, depending on what it does. It's good practice to cover every exception you expect to happen. For example if you're using a command which takes the content ID as a parameter, the ID may either not exist, or the referenced content item may not be visible to the user. Both cases should be covered with error messages: ```php try { // ... } catch (\Ibexa\Contracts\Core\Repository\Exceptions\NotFoundException) { $output->writeln("No content with id $contentId found"); } catch (\Ibexa\Contracts\Core\Repository\Exceptions\UnauthorizedException) { $output->writeln("Permission denied on content with id $contentId"); } ``` ## Service container Cohesivo uses the [Symfony service container](https://symfony.com/doc/7.4/service_container.html) for dependency resolution. [Symfony dependency injection](https://symfony.com/doc/7.4/service_container.html) ensures that any required services are available in your custom code (for example, controllers) when you inject them into the constructor. Symfony service container uses service tags to dedicate services to a specific purpose. They're usually used for extension points. Cohesivo uses service tags to expose multiple features. For example, field types are tagged `ibexa.field_type`. > **Tip: Tip** > > For a list of all service tags exposed by Symfony, see its [reference documentation](https://symfony.com/doc/7.4/reference/dic_tags.html). # REST API usage > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). The REST API covers objects in the Cohesivo Repository with regular and custom HTTP methods, such as GET or PUBLISH, and HTTP headers. The REST API in Cohesivo allows you to interact with the Cohesivo installation by using the HTTP protocol, following a [REST](https://en.wikipedia.org/wiki/Representational_state_transfer) interaction model. Each resource (URI) interacts with a part of the system (like content, users or search). Every interaction with the repository than you can do from back office or by using the [Public PHP API](https://doc.ibexa.co/en/saas/api/php_api/php_api/index.md) can also be done with the REST API. The REST API uses HTTP methods (such as `GET` and `PUBLISH`), and HTTP headers to specify the type of request. ## OpenAPI support The REST API is built on top of [API Platform](https://api-platform.com/docs/symfony/) and meets the [OpenAPI](https://www.openapis.org/) standard. You can download the OpenAPI specification in: - [YAML format](https://doc.ibexa.co/en/saas/api/rest_api/rest_api_reference/openapi.yaml) - [JSON format](https://doc.ibexa.co/en/saas/api/rest_api/rest_api_reference/openapi.json) You can also generate one for your project by running one of the commands below: ```bash php bin/console ibexa:openapi --output=openapi.json # JSON output php bin/console ibexa:openapi --yaml --output=openapi.yaml # YAML output ``` Use the specification file with [available OpenAPI tools](https://tools.openapis.org/) to work faster with the API, for example, by generating libraries and clients for the API. > **Note: Note** > > In [Symfony's `dev` environment](https://doc.ibexa.co/en/saas/infrastructure_and_maintenance/environments/index.md), you can access a REST API reference generated for your project by visiting the `/api/ibexa/v2/doc` route in the browser. ## URIs The REST API is designed in such a way that the client can explore the Repository without constructing any URIs to resources. Starting from the [root resource](#rest-root), every response includes further links (`href`) to related resources. ### URI prefix [REST reference](https://doc.ibexa.co/en/saas/api/rest_api/rest_api_reference/rest_api_reference.html), for the sake of readability, uses no prefixes in the URIs. In practice, the `/api/ibexa/v2` prefixes all REST hrefs. This prefix immediately follows the domain, and you can't use the [`URIElement` SiteAccess matcher](https://doc.ibexa.co/en/saas/multisite/siteaccess/siteaccess_matching/#urielement). If you need to the select a SiteAccess, see the [`X-Siteaccess` HTTP header](https://doc.ibexa.co/en/saas/api/rest_api/rest_api_usage/rest_requests/#siteaccess). ### URI parameters URI parameters (query string) can be used on some resources. They usually serve as options or filters for the requested resource. As an example, the request below would paginate the results and return the first 5 relations for version 3 of the content item 59: ```http GET /content/objects/59/versions/3/relations?limit=5 HTTP/1.1 Accept: application/vnd.ibexa.api.RelationList+xml ``` #### Working with value objects IDs Resources that accept a reference to another resource expect the reference to be given as a REST URI, not a single ID. For example, the URI requesting a list of user groups assigned to the role with ID 1 is: ```http GET /api/ibexa/v2/user/groups?roleId=/api/ibexa/v2/user/roles/1 HTTP/1.1 ``` ### REST root The `/` root route is answered by a reference list with the main resource routes and media-types. It's presented in XML by default, but you can also switch to JSON output. ```bash curl https://api.example.com/api/ibexa/v2/ curl -H "Accept: application/json" https://api.example.com/api/ibexa/v2/ ``` ### Country list Alongside regular Repository interactions, there is a REST service providing a list of countries with their names, [ISO-3166](https://en.wikipedia.org/wiki/ISO_3166) codes and International Dialing Codes (IDC). You can use it when presenting a country options list from any application. This country list's URI is `/services/countries`. The ISO-3166 country codes can be represented as: - two-letter code (alpha-2) — recommended as the general purpose code - three-letter code (alpha-3) — related to the country name - three-digit numeric code (numeric-3) — use it if you need to avoid using Latin script For details, see the [ISO-3166 glossary](https://www.iso.org/glossary-for-iso-3166.html). ## REST communication summary - A REST route (URI) leads to a REST controller action. A REST route is composed of the root prefix (`ibexa.rest.path_prefix: /api/ibexa/v2`) and a resource path (for example, `/content/objects/{contentId}`). - This controller action returns an `Ibexa\Rest\Value` descendant. - This controller action might use the `Request` to build its result according to, for example, GET parameters, the `Accept` HTTP header, or the request payload and its `Content-Type` HTTP header. - This controller action might wrap its return in a `CachedValue` which contains caching information for the reverse proxies. - The `Ibexa\Bundle\Rest\EventListener\ResponseListener` attached to the `kernel.view event` is triggered, and passes the request and the controller action's result to the `AcceptHeaderVisitorDispatcher`. - The `AcceptHeaderVisitorDispatcher` matches one of the `regexps` of an `ibexa.rest.output.visitor` service (an `Ibexa\Contracts\Rest\Output\Visitor`). The role of this `Output\Visitor` is to transform the value returned by the controller into XML or JSON output format. To do so, it combines an `Output\Generator` corresponding to the output format and a `ValueObjectVisitorDispatcher`. This `Output\Generator` is also adding the `media-type` attributes. - The matched `Output\Visitor` uses its `ValueObjectVisitorDispatcher` to select the right `ValueObjectVisitor` according to the fully qualified class name (FQCN) of the controller result. A `ValueObjectVisitor` is a service tagged `ibexa.rest.output.value_object.visitor` and this tag has a property `type` pointing a FQCN. - `ValueObjectVisitor`s recursively help to transform the controller result thanks to the abstraction layer of the `Generator`. - The `Output\Visitor` returns the `Response` to send back to the client. # REST requests > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). REST API requests can have a generic or a custom header. It defines additional options in the request, such as the accepted content type of response. ## Request method Depending on the HTTP method used, different actions are possible on the same resource. Example: | Action | Description | | --------------------------------------- | ------------------------------------------------------------------ | | `GET /content/objects/2/versions/3` | Fetches data about version #3 of content item #2 | | `PATCH /content/objects/2/versions/3` | Updates the version #3 draft of content item #2 | | `DELETE /content/objects/2/versions/3` | Deletes the (draft or archived) version #3 from content item #2 | | `COPY /content/objects/2/versions/3` | Creates a new draft version of content item #2 from its version #3 | | `PUBLISH /content/objects/2/versions/3` | Promotes the version #3 of content item #2 from draft to published | | `OPTIONS /content/objects/2/versions/3` | Lists all the methods usable with this resource, the 5 ones above | The following list of available methods gives an overview of the kind of action a method triggers on a resource, if available. For method action details per resource, see the [REST API reference](https://doc.ibexa.co/en/saas/api/rest_api/rest_api_reference/rest_api_reference.html). | HTTP method | Status | Description | Safe | | -------------------------------------------------------------------- | -------- | ---------------------- | ---- | | [OPTIONS](https://datatracker.ietf.org/doc/html/rfc2616#section-9.2) | Standard | List available methods | Yes | | [GET](https://datatracker.ietf.org/doc/html/rfc2616#section-9.3) | Standard | Collect data | Yes | | [HEAD](https://datatracker.ietf.org/doc/html/rfc2616#section-9.4) | Standard | Check existence | Yes | | [POST](https://datatracker.ietf.org/doc/html/rfc2616#section-9.5) | Standard | Create an item | No | | [PATCH](https://datatracker.ietf.org/doc/html/rfc5789) | Custom | Update an item | No | | COPY | Custom | Duplicate an item | No | | [MOVE](https://datatracker.ietf.org/doc/html/rfc2518) | Custom | Move an item | No | | SWAP | Custom | Swap two locations | No | | PUBLISH | Custom | Publish an item | No | | [DELETE](https://datatracker.ietf.org/doc/html/rfc2616#section-9.7) | Standard | Remove an item | No | > **Note: Caution with custom HTTP methods** > > Using custom HTTP methods can cause issues with several HTTP proxies, network firewall/security solutions and simpler web servers. To avoid such issuess, REST API allows you to set these by using the HTTP header `X-HTTP-Method-Override` alongside the standard `POST` method instead of using a custom HTTP method. For example: `X-HTTP-Method-Override: PUBLISH` > > If applicable, both methods are always mentioned in the specifications. Unsafe methods require a CSRF token if [session-based authentication](https://doc.ibexa.co/en/saas/api/rest_api/rest_api_authentication/#session-based-authentication) is used. ### OPTIONS method Any REST API URI responds to an `OPTIONS` request. The response contains an [`Allow` header](https://www.rfc-editor.org/rfc/rfc9110.html#name-allow), which lists the methods accepted by the resource. ```bash curl -IX OPTIONS https://api.example.com/api/ibexa/v2/content/objects/1 ``` ```http OPTIONS /content/objects/1 HTTP/1.1 Host: api.example.com ``` ```http HTTP/1.1 200 OK Allow: PATCH,GET,DELETE,COPY ``` ```bash curl -IX OPTIONS https://api.example.com/api/ibexa/v2/content/locations/1/2 ``` ```http OPTIONS /content/locations/1/2 HTTP/1.1 Host: api.example.com ``` ```http HTTP/1.1 200 OK Allow: GET,PATCH,DELETE,COPY,MOVE,SWAP ``` ## Request headers You can use the following HTTP headers with a REST request: - [`Accept`](https://datatracker.ietf.org/doc/html/rfc2616#section-14.1) describing the desired response type and format - [`Content-Type`](https://datatracker.ietf.org/doc/html/rfc2616#section-14.17) describing the payload type and format - [`X-Siteaccess`](#siteaccess) specifying the target SiteAccess - `X-HTTP-Method-Override` allowing to pass a custom method while using `POST` method as previously seen in [HTTP method](#request-method) - [`Destination`](#destination) specifying where to move an item - [`X-Expected-User`](#expected-user) specifying the user needed for the request execution Other headers related to authentication methods can be found in [REST API authentication](https://doc.ibexa.co/en/saas/api/rest_api/rest_api_authentication/index.md). ### SiteAccess To specify a SiteAccess when communicating with the REST API, provide a custom `X-Siteaccess` header. Otherwise, the default SiteAccess is used. The following example shows what could be a SiteAccess called `restapi` dedicated to REST API accesses: ```http GET / HTTP/1.1 Host: api.example.com Accept: application/vnd.ibexa.api.Root+json X-Siteaccess: restapi ``` One of the principles of REST is that the same resource (such as content item, location, content type) should be unique. It allows caching your REST API with a reverse proxy such as Varnish. If the same resource is available in multiple locations, cache purging is noticeably more complex. This is why SiteAccess matching with REST isn't enabled at URL level (or domain). ### Media types On top of methods, HTTP request headers allow you to personalize the request's behavior. On every resource, you can use the `Accept` header to indicate which format you want to communicate in, JSON or XML. This header is also used to specify the response type you want the server to send when multiple types are available. - `Accept: application/vnd.ibexa.api.Content+xml` to get `Content` (full data, fields included) as **[XML](https://www.w3.org/XML/)** - `Accept: application/vnd.ibexa.api.ContentInfo+json` to get `ContentInfo` (metadata only) as **[JSON](https://www.json.org/)** Media types are also used with the [`Content-Type` header](https://doc.ibexa.co/en/saas/api/rest_api/rest_api_usage/rest_responses/#content-type-header) to characterize a [request body](#request-body) or a [response body](https://doc.ibexa.co/en/saas/api/rest_api/rest_api_usage/rest_responses/#response-body). See [Creating content with binary attachments](#creating-content-with-binary-attachments) below. Also see [Creating session](https://doc.ibexa.co/en/saas/api/rest_api/rest_api_authentication/#creating-session) examples. If the resource only returns one media type, it's also possible to skip it and to specify the format with `application/xml` or `application/json`. A response indicates `href`s to related resources and their media types. ### Destination The `Destination` request header is the request counterpart of the `Location` response header. It's used for a `COPY`, `MOVE` or `SWAP` operation to indicate where the resource should be moved, copied to or swapped with by using the ID of the parent or target location. Examples of such requests are: - [copying a Content](https://doc.ibexa.co/en/saas/api/rest_api/rest_api_reference/rest_api_reference.html#managing-content-copy-content) - [moving a Location and its subtree](https://doc.ibexa.co/en/saas/api/rest_api/rest_api_reference/rest_api_reference.html#managing-content-move-subtree) - [swapping a Location with another](https://doc.ibexa.co/en/saas/api/rest_api/rest_api_reference/rest_api_reference.html#managing-content-swap-location) ### Expected user The `X-Expected-User` header specifies the user needed for the request execution. With this header, if the current username on server side isn't equal to `X-Expected-User` value, a `401 Unauthorized` error is returned. Without this header, the request is executed with the current user who might be unexpected (like the Anonymous user if a previous authentication has expired) and an ambiguous response might be returned as a success not informing about a wrong user. For example, it prevents a Content request to be executed with Anonymous user in the case of an expired authentication, and the response being a `200 OK` but missing content items due to access rights difference with the expected user. ## Request body You can pass some short scalar parameters in the URIs or as GET parameters, but other resources need heavier structured payloads passed in the request body, in particular the ones to create (`POST`) or update (`PATCH`) items. In the [REST API reference](https://doc.ibexa.co/en/saas/api/rest_api/rest_api_reference/rest_api_reference.html), request payload examples are given when needed. One example is the [creation of an authentication session](https://doc.ibexa.co/en/saas/api/rest_api/rest_api_authentication/#establishing-session). When creating a content item, a special payload is needed if the content type has some [Image](https://doc.ibexa.co/en/saas/content_management/field_types/field_type_reference/imagefield/index.md) or [BinaryFile](https://doc.ibexa.co/en/saas/content_management/field_types/field_type_reference/binaryfilefield/index.md) fields as files need to be attached. See the example of a [script uploading images](#creating-content-with-binary-attachments) below. When searching for content items (or locations), the query grammar is also particular. See the [Search section](#search-views) below. ### Creating content with binary attachments The example below is a command-line script to upload images. It's based on the [Symfony HttpClient](https://symfony.com/doc/7.4/http_client.html). This script: - receives an image path and optionally a name as command-line arguments, - uses the [HTTP basic authentication](https://doc.ibexa.co/en/saas/api/rest_api/rest_api_authentication/#http-basic-authentication), if it's enabled, - creates a draft in the /Media/Images folder by posting (`POST`) data to [`/content/objects`](https://doc.ibexa.co/en/saas/api/rest_api/rest_api_reference/rest_api_reference.html#tag/Objects/operation/api_contentobjects_post), - and, publishes (`PUBLISH`) the draft through [`/content/objects/{contentId}/versions/{versionNo}`](https://doc.ibexa.co/en/saas/api/rest_api/rest_api_reference/rest_api_reference.html#managing-content-publish-a-content-version). **XML** ```php []\n"; exit(1); } if (!is_file($argv[1])) { echo "{$argv[1]} doesn't exist or is not a file.\n"; exit(2); } // URL to Ibexa DXP installation and its REST API $host = 'api.example.com'; $scheme = 'https'; $api = '/api/ibexa/v2'; $baseUrl = "{$scheme}://{$host}{$api}"; // User credentials $username = 'admin'; $password = 'publish'; // Targets $contentTypeId = 5; // "Image" $parentLocationPath = '1/43/51'; // "Media > Images" $sectionId = 3; // "Media" $fileName = basename($argv[1]); $fileSize = filesize($argv[1]); $fileContent = base64_encode(file_get_contents($argv[1])); $name = $argv[2] ?? $fileName; // Request payload $data = << eng-GB PATH ASC
name $name caption

$name

]]> image $fileName $fileSize
XML; $client = HttpClient::createForBaseUri($baseUrl, [ 'auth_basic' => [$username, $password], ]); $doc = new DOMDocument(); try { $response = $client->request('POST', "$baseUrl/content/objects", [ 'headers' => [ 'Content-Type: application/vnd.ibexa.api.ContentCreate+xml', 'Accept: application/vnd.ibexa.api.ContentInfo+xml', ], 'body' => $data, ]); } catch (HttpException\TransportExceptionInterface $exception) { echo "Client error: {$exception->getMessage()}\n"; exit(3); } if (201 !== $responseCode = $response->getStatusCode()) { if (!empty($response->getContent(false)) && $doc->loadXML($response->getContent(false)) && 'ErrorMessage' === $doc->firstChild->nodeName) { echo "Server error: {$doc->getElementsByTagName('errorCode')->item(0)->nodeValue} {$doc->getElementsByTagName('errorMessage')->item(0)->nodeValue}\n"; echo "\t{$doc->getElementsByTagName('errorDescription')->item(0)->nodeValue}\n"; exit(4); } $responseHeaders = $response->getInfo('response_headers'); $error = $responseHeaders[0] ?? $responseCode; echo "Server error: $error\n"; exit(5); } $doc->loadXML($response->getContent()); if ('Content' !== $doc->firstChild->nodeName || !$doc->firstChild->hasAttribute('id')) { echo "Response error: Unexpected response structure\n"; exit(6); } $contentId = $doc->firstChild->getAttribute('id'); try { $response = $client->request('PUBLISH', "$baseUrl/content/objects/$contentId/versions/1", [ 'headers' => [ 'Accept: application/xml', ], ]); } catch (HttpException\TransportExceptionInterface $exception) { echo "Client error: {$exception->getMessage()}\n"; exit(7); } if (204 !== $responseCode = $response->getStatusCode()) { if (!empty($response->getContent(false)) && $doc->loadXML($response->getContent(false)) && 'ErrorMessage' === $doc->firstChild->nodeName) { echo "Server error: {$doc->getElementsByTagName('errorCode')->item(0)->nodeValue} {{$doc->getElementsByTagName('errorMessage')->item(0)->nodeValue}\n"; echo "\t{$doc->getElementsByTagName('errorDescription')->item(0)->nodeValue}\n"; exit(8); } $responseHeaders = $response->getInfo('response_headers'); $error = $responseHeaders[0] ?? $responseCode; echo "Server error: $error\n"; exit(9); } echo "Success: Image content item created with ID $contentId and published.\n"; exit(0); ``` **JSON** ```php []\n"; exit(1); } if (!is_file($argv[1])) { echo "{$argv[1]} doesn't exist or is not a file.\n"; exit(2); } // URL to Ibexa DXP installation and its REST API $host = 'api.example.com'; $scheme = 'https'; $api = '/api/ibexa/v2'; $baseUrl = "{$scheme}://{$host}{$api}"; // User credentials $username = 'admin'; $password = 'publish'; // Targets $contentTypeId = 5; // "Image" $parentLocationPath = '1/43/51'; // "Media > Images" $sectionId = 3; // "Media" // Request payload $data = [ 'ContentCreate' => [ 'ContentType' => [ '_href' => "$api/content/types/$contentTypeId", ], 'mainLanguageCode' => 'eng-GB', 'LocationCreate' => [ 'ParentLocation' => [ '_href' => "$api/content/locations/$parentLocationPath", ], 'sortField' => 'PATH', 'sortOrder' => 'ASC', ], 'Section' => [ '_href' => "$api/content/sections/$sectionId", ], 'fields' => [ 'field' => [ [ 'fieldDefinitionIdentifier' => 'name', 'fieldValue' => $argv[2] ?? basename($argv[1]), ], [ 'fieldDefinitionIdentifier' => 'image', 'fieldValue' => [ // Original file name 'fileName' => basename($argv[1]), // File size in bytes 'fileSize' => filesize($argv[1]), // File content must be encoded as Base64 'data' => base64_encode(file_get_contents($argv[1])), ], ], ], ], ], ]; $client = HttpClient::createForBaseUri($baseUrl, [ 'auth_basic' => [$username, $password], ]); try { $response = $client->request('POST', "$baseUrl/content/objects", [ 'headers' => [ 'Content-Type: application/vnd.ibexa.api.ContentCreate+json', 'Accept: application/vnd.ibexa.api.ContentInfo+json', ], 'json' => $data, ]); } catch (HttpException\TransportExceptionInterface $exception) { echo "Client error: {$exception->getMessage()}\n"; exit(3); } if (201 !== $responseCode = $response->getStatusCode()) { try { $responseArray = $response->toArray(false); if (array_key_exists('ErrorMessage', $responseArray)) { echo "Server error: {$responseArray['ErrorMessage']['errorCode']} {$responseArray['ErrorMessage']['errorMessage']}\n"; echo "\t{$responseArray['ErrorMessage']['errorDescription']}\n"; exit(4); } } catch (HttpException\DecodingExceptionInterface) { } $responseHeaders = $response->getInfo('response_headers'); $error = $responseHeaders[0] ?? $responseCode; echo "Server error: $error\n"; exit(5); } $response = $response->toArray(); if (!(array_key_exists('Content', $response) && array_key_exists('_id', $response['Content']))) { echo "Response error: Unexpected response structure\n"; exit(6); } $contentId = $response['Content']['_id']; try { $response = $client->request('PUBLISH', "$baseUrl/content/objects/$contentId/versions/1", [ 'headers' => [ 'Accept: application/json', ], ]); } catch (HttpException\TransportExceptionInterface $exception) { echo "Client error: {$exception->getMessage()}\n"; exit(7); } if (204 !== $responseCode = $response->getStatusCode()) { try { $responseArray = $response->toArray(false); if (array_key_exists('ErrorMessage', $responseArray)) { echo "Server error: {$responseArray['ErrorMessage']['errorCode']} {$responseArray['ErrorMessage']['errorMessage']}\n"; echo "\t{$responseArray['ErrorMessage']['errorDescription']}\n"; exit(8); } } catch (HttpException\DecodingExceptionInterface) { } $responseHeaders = $response->getInfo('response_headers'); $error = $responseHeaders[0] ?? $responseCode; echo "Server error: $error\n"; exit(9); } echo "Success: Image content item created with ID $contentId and published.\n"; exit(0); ``` ### Search (`/views`) The `/views` route allows you to [search in the repository](https://doc.ibexa.co/en/saas/search/search/index.md). It works similarly to its [PHP API counterpart](https://doc.ibexa.co/en/saas/search/search_api/index.md). The model allows combining criteria using the logical operators `AND`, `OR` and `NOT`. Most [Search Criteria](https://doc.ibexa.co/en/saas/search/criteria_reference/search_criteria_reference/#search-criteria) are available in REST API. The suffix `Criterion` is added when used with REST API. Most [Sort Clauses](https://doc.ibexa.co/en/saas/search/sort_clause_reference/sort_clause_reference/#sort-clauses) are available too. They require no additional prefix or suffix. The search request has a `Content-Type: application/vnd.ibexa.api.ViewInput+xml` or `+json` header to specify the format of its body's payload. The root node is `` and it has two mandatory children: `` and ``. You can add `version=1.1` to the `Content-Type` header to support the distinction between `ContentQuery` and `LocationQuery` instead of `Query` which implicitly looks only for content items. The following examples search for `article` and `news` typed content items everywhere or for content items of all types directly under location `123`. All those content items must be in the `standard` section. **XML** ```http POST /views HTTP/1.1 Content-Type: application/vnd.ibexa.api.ViewInput+xml ``` ```xml test article news 123 standard 10 0 ascending ``` **XML; 1.1** ```http POST /views HTTP/1.1 Content-Type: application/vnd.ibexa.api.ViewInput+xml; version=1.1 ``` ```xml test article news 123 standard 10 0 ascending ``` **JSON** ```http POST /views HTTP/1.1 Content-Type: application/vnd.ibexa.api.ViewInput+json ``` ```json { "ViewInput": { "identifier": "test", "Query": { "Filter": { "AND": { "OR": { "ContentTypeIdentifierCriterion": [ "article", "news" ], "ParentLocationIdCriterion": 123 }, "SectionIdentifierCriterion": "standard" } }, "limit": "10", "offset": "0", "SortClauses": { "ContentName": "ascending" } } } } ``` **JSON; 1.1** ```http POST /views HTTP/1.1 Content-Type: application/vnd.ibexa.api.ViewInput+json; version=1.1 ``` ```json { "ViewInput": { "identifier": "test", "ContentQuery": { "Filter": { "AND": { "OR": { "ContentTypeIdentifierCriterion": [ "article", "news" ], "ParentLocationIdCriterion": 123 }, "SectionIdentifierCriterion": "standard" } }, "limit": "10", "offset": "0", "SortClauses": { "ContentName": "ascending" } } } } ``` > **Note: Note** > > In JSON, the structure for `ContentTypeIdentifierCriterion` with multiple values has a slightly different format as keys must be unique. In JSON, if there is only one item in `SortClauses`, it can be passed directly without an array to wrap it. You can omit logical operators. If Criteria are of mixed types, they're wrapped in an implicit `AND`. If they're of the same type, they're wrapped in an implicit `OR`. For example, the `AND` operator from previous example's `Filter` could be removed. **XML ExplicitAND** ```xml article news 123 standard ``` **XML ImplicitAND** ```xml article news 123 standard ``` **JSON ExplicitAND** ```json "Filter": { "AND": { "OR": { "ContentTypeIdentifierCriterion": [ "article", "news" ], "ParentLocationIdCriterion": 123 }, "SectionIdentifierCriterion": "standard" } }, ``` **JSON ImplicitAND** ```json "Filter": { "OR": { "ContentTypeIdentifierCriterion": [ "article", "news" ], "ParentLocationIdCriterion": 123 }, "SectionIdentifierCriterion": "standard" }, ``` # REST Responses > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). REST API response code defines the status of the received response. ## Response code The following list of available HTTP response status codes gives an overview of the meaning of each code. For code details per resource, see the [REST API reference](https://doc.ibexa.co/en/saas/api/rest_api/rest_api_reference/rest_api_reference.html). | Code | Message | Description | | ----- | ---------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `200` | OK | The resource has been found. | | `201` | Created | The request to create a new item has succeeded. The response `Location` header indicates where you can find the created item. | | `204` | No Content | The request has succeeded and there is no additional information in the response header or body (for example when publishing or deleting). | | `301` | Moved Permanently | The resource shouldn't be accessed this way. The response `Location` header indicates the proper way. | | `307` | Temporary Redirect | The resource is available at another URL considered as its main. The response `Location` header indicates this main URL. | | `400` | Bad Request | The input (payload) doesn't have the proper schema for the resource. | | `401` | Unauthorized | The user doesn't have the permission to make this request. | | `403` | Forbidden | The user has the permission but action can't be performed because of Repository logic (for example, when trying to create an item with an already existing ID or identifier, when attempting to update a version in another state than draft). | | `404` | Not Found | The requested object (or a request data like the parent of a new item) hasn't been found. | | `405` | Method Not Allowed | The requested resource doesn't support the HTTP verb that was used. | | `406` | Not Acceptable | The request's `Accept` header isn't supported. | | `409` | Conflict | The request is in conflict with another part of the repository (for example, trying to create a new item with an identifier already used). | | `415` | Unsupported Media Type | The request payload media type doesn't match the media type specified in the request header. | | `500` | Internal Server Error | The server encountered an unexpected condition, usually an exception, which prevents it from fulfilling the request, like database down, permissions or configuration error. | | `501` | Not Implemented | Returned when the requested method hasn't yet been implemented. For Cohesivo, most of users, user groups, content items, locations and content types have been implemented. Some of their methods, and other features, may return a 501. | ## Response headers A resource's response may contain metadata in its HTTP headers. > **Note: Note** > > For information about the `Allow` response header, see the [`OPTIONS` method](https://doc.ibexa.co/en/saas/api/rest_api/rest_api_usage/rest_requests/#options-method). ### Content-Type header When a response contains an actual HTTP body, the `Content-Type` header specifies what the body contains. The `Content-Type` header's value is a [media type](https://doc.ibexa.co/en/saas/api/rest_api/rest_api_usage/rest_requests/#media-types), like with the request `Accept` and `Content-Type` headers. For example, the first following request without an `Accept` header returns a default format indicated in the response `Content-Type` header, while the second request shows that the response is in the requested format. ```http GET /content/objects/52 HTTP/1.1 ``` ```http HTTP/1.1 200 OK Content-Type: application/vnd.ibexa.api.ContentInfo+xml ``` ```http GET /content/objects/52 HTTP/1.1 Accept: application/vnd.ibexa.api.Content+json ``` ```http HTTP/1.1 200 OK Content-Type: application/vnd.ibexa.api.Content+json ``` ### Accept-Patch header When available, the `Accept-Patch` tells how the received item could be modified with `PATCH`. The following examples also shows that the format (XML or JSON) is adapted: ```http GET /content/objects/52 HTTP/1.1 ``` ```http HTTP/1.1 200 OK Content-Type: application/vnd.ibexa.api.ContentInfo+xml Accept-Patch: application/vnd.ibexa.api.ContentUpdate+xml ``` ```http GET /content/objects/52 HTTP/1.1 Accept: application/vnd.ibexa.api.Content+json ``` ```http HTTP/1.1 200 OK Content-Type: application/vnd.ibexa.api.Content+json Accept-Patch: application/vnd.ibexa.api.ContentUpdate+json ``` Those example `Accept-Path` headers above indicate that the content could be modified by sending a [ContentUpdateStruct](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-Core-Repository-Values-Content-ContentUpdateStruct.html) in XML or JSON. ### Location header For example, [creating content](https://doc.ibexa.co/en/saas/api/rest_api/rest_api_reference/rest_api_reference.html#managing-content-create-content-type) and [getting a content item's current version](https://doc.ibexa.co/en/saas/api/rest_api/rest_api_reference/rest_api_reference.html#tag/Objects/operation/api_contentobjects_contentIdcurrentversion_get) both send a `Location` header to provide you with the requested resource's ID. Those particular headers generally match a specific list of HTTP response codes. `Location` is mainly sent alongside `201 Created`, `301 Moved permanently`, `307 Temporary redirect responses`. In the following example, the content item's remote ID 34720ff636e1d4ce512f762dc638e4ac corresponds to the ID 52: ```http GET /content/objects?remoteId=34720ff636e1d4ce512f762dc638e4ac" HTTP/1.1 ``` ```http HTTP/1.1 307 Temporary Redirect Location: /content/objects/52 ``` In the following example, an erroneous slash has been added to demonstrate the 301 case: ```http GET /content/objects?remoteId=34720ff636e1d4ce512f762dc638e4ac" HTTP/1.1 ``` ```http HTTP/1.1 301 Moved Permanently Location: /content/objects?remoteId=34720ff636e1d4ce512f762dc638e4ac ``` cURL can follow those redirections. On CLI, there is the `--location` option (or its shorthand `-L`). In PHP, you can achieve the same effect with `CURLOPT_FOLLOWLOCATION`. The following command-line example follows the two redirections above and the `Accept` header is propagated: ```bash curl --head --location --header "Accept: application/vnd.ibexa.api.Content+json" "https://api.example.com/api/ibexa/v2/content/objects/?remoteId=34720ff636e1d4ce512f762dc638e4ac" ``` ```http HTTP/1.1 200 OK Content-Type: application/vnd.ibexa.api.Content+json ``` ### Cross-origin [Cross-Origin Resource Sharing (CORS)](https://en.wikipedia.org/wiki/Cross-origin_resource_sharing) can allow the REST API to be reached from a page on another domain. For more information about CORS, see [WHATWG's CORS Protocol specification](https://fetch.spec.whatwg.org/#cors-protocol) and [Overview of CORS on developer.mozilla.org](https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/CORS). CORS support is provided by the third party [nelmio/cors-bundle](https://packagist.org/packages/nelmio/cors-bundle). You can read more about it in [NelmioCorsBundle's README](https://github.com/nelmio/NelmioCorsBundle/blob/master/README.md). Using CORS isn't limited to REST API resources and can be used for any resource of the platform. The CORS bundle adds an `Access-Control-Allow-Origin` header to the response. #### Configuration To enable CORS, add regular expression for an allowed domain using the `.env` variable `CORS_ALLOW_ORIGIN`. For example, to allow the [JS test](https://doc.ibexa.co/en/saas/api/rest_api/rest_api_usage/testing_rest_api/#js) to be executed alongside this page, you could add the following to an `.env` file (like the `.env.local`): `CORS_ALLOW_ORIGIN=^https?://doc.ibexa.co`. To add several domains, filter on URIs, or change the default (like not allowing all the methods), refer to [NelmioCorsBundle Configuration Documentation](https://symfony.com/bundles/NelmioCorsBundle/current/index.html#configuration) to learn how to edit `config/packages/nelmio_cors.yaml`. ## Response body The Response body is often a serialization in XML or JSON of an object as it could be retrieved using the Public PHP API. For example, the resource `/content/objects/52` with the `Accept: application/vnd.ibexa.api.ContentInfo+xml` header returns a serialized version of a [ContentInfo](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-Core-Repository-Values-Content-ContentInfo.html) object. ```bash curl https://api.example.com/content/objects/52 --header 'Accept: application/vnd.ibexa.api.ContentInfo+xml'; ``` ```xml Ibexa Digital Experience Platform Ibexa Digital Experience Platform
2015-09-17T09:22:23+00:00 2015-09-17T09:22:23+00:00 eng-GB 1 true false PUBLISHED ``` The response body XML can contain two types of nodes: - Final nodes that fully give an information as a scalar value - Reference nodes which link to `href` where a new resource of a given `media-type` can be explored if you need to know more # Testing REST API > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). You can test operations in the REST API by using command line, PHP or JS code. A standard web browser isn't sufficient to fully test the API. You can, however, try opening the root resource with it, using the session authentication: `http://example.com/api/ibexa/v2/`. Depending on how your browser understands XML, it either downloads the XML file, or opens it in the browser. The following examples show how to interrogate the REST API with cURL, PHP or JS. ## CLI For examples of using `curl`, refer to: - [REST root](https://doc.ibexa.co/en/saas/api/rest_api/rest_api_usage/rest_api_usage/#rest-root) - [OPTIONS method](https://doc.ibexa.co/en/saas/api/rest_api/rest_api_usage/rest_requests/#options-method) - [Location header](https://doc.ibexa.co/en/saas/api/rest_api/rest_api_usage/rest_responses/#location-header) - [ContentInfo body](https://doc.ibexa.co/en/saas/api/rest_api/rest_api_usage/rest_responses/#response-body) ## PHP You can use [Symfony HttpClient](https://symfony.com/doc/7.4/http_client.html) to test REST API. Open a PHP shell in a terminal with `php -a` and copy-paste this code into it: ```php $resource = 'https://api.example.com/api/ibexa/v2/content/objects/52'; require 'vendor/autoload.php'; $client = Symfony\Component\HttpClient\HttpClient::create(); $response = $client->request('GET', $resource, [ 'headers' => ['Accept: application/vnd.ibexa.api.ContentInfo+json'], ]); var_dump($response->getStatusCode(), $response->getHeaders(), $response->toArray()); ``` `$resource` URI should be edited to address the right domain. On a freshly installed Cohesivo, `52` is the Content ID of the home page. If necessary, substitute `52` with the content ID of an item from your database. For a content creation example that uses PHP, see [Creating content with binary attachments](https://doc.ibexa.co/en/saas/api/rest_api/rest_api_usage/rest_requests/#creating-content-with-binary-attachments) ## JS The REST API can help you implement JavaScript / AJAX interaction. The following example of an AJAX call retrieves `ContentInfo` (that is, metadata) for a content item. To test it, copy-paste this code into your browser console alongside a page from your website (to share the domain): **Fetch API** ```javascript const resource = '/api/ibexa/v2/content/objects/52'; fetch(resource, { headers: {'Accept': 'application/vnd.ibexa.api.ContentInfo+json'}, }).then((response) => { console.log(...response.headers); return response.json(); }).then((data) => { console.log(data); }); ``` **XMLHttpRequest** ```javascript const resource = '/api/ibexa/v2/content/objects/52'; const request = new XMLHttpRequest(); request.open('GET', resource, true); request.setRequestHeader('Accept', 'application/vnd.ibexa.api.ContentInfo+json'); request.onload = function () { console.log(request.getAllResponseHeaders(), JSON.parse(request.responseText)); }; request.send(); ``` On a freshly installed Cohesivo, `52` is the Content ID of the home page. If necessary, substitute `52` with the Content ID of an item from your database. You can edit the `resource` URI to address another domain, but [cross-origin requests](https://doc.ibexa.co/en/saas/api/rest_api/rest_api_usage/rest_responses/#cross-origin) must be allowed first. # Adding custom media type > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). Add a custom media type to REST API request headers. In this example case, you pass a new media type in the `Accept` header of a GET request to `/content/locations/{locationPath}` route and its controller action (`Controller/Location::loadLocation`). By default, this resource takes an `application/vnd.ibexa.api.Location+xml` (or `+json`) `Accept` header. The following example adds the handling of a new media type `application/app.api.Location+xml` (or `+json`) `Accept` header to obtain a different response with the same controller. You need the following elements: - `ValueObjectVisitor` - to create the new response corresponding to the new media type - `ValueObjectVisitorDispatcher` - to have this `ValueObjectVisitor` used to visit the default controller result - `Output\Visitor` - service associating this new `ValueObjectVisitorDispatcher` with the new media type > **Note: Note** > > You can change the vendor name (from default `vnd.ibexa.api` to new `app.api` like in this example), or you can create a new media type in the default vendor (like `vnd.ibexa.api.Greeting` in the [Creating a new REST resource](https://doc.ibexa.co/en/saas/api/rest_api/extending_rest_api/creating_new_rest_resource/index.md) example). To do so, tag your new ValueObjectVisitor with `ibexa.rest.output.value_object.visitor` to add it to the existing `ValueObjectVisitorDispatcher`, and a new one isn't needed. This way, the `media-type` attribute is also easier to create, because the default `Output\Generator` uses this default vendor. This example presents creating a new vendor as a good practice, to highlight that this is custom extensions that isn't available in a regular Cohesivo installation. ## New `RestLocation` `ValueObjectVisitor` The controller action returns a `Values\RestLocation` object wrapped in `Values\CachedValue`. The new `ValueObjectVisitor` has to visit `Values\RestLocation` to prepare the new `Response`. To be accepted by the `ValueObjectVisitorDispatcher`, all new `ValueObjectVisitor` need to extend the abstract class `Output\ValueObjectVisitor`. In this example, this new `ValueObjectVisitor` extends the built-in `RestLocation` visitor to reuse it. This way, the abstract class is implicitly inherited. ```php startObjectElement to not have the XML Generator adding its own media-type attribute with the default vendor $generator->startHashElement('Location'); $generator->attribute( 'media-type', 'application/app.api.Location+' . strtolower((new \ReflectionClass($generator))->getShortName()) ); $generator->attribute( 'href', $this->router->generate( 'ibexa.rest.load_location', ['locationPath' => trim($data->location->pathString, '/')] ) ); parent::visit($visitor, $generator, $data); $visitor->visitValueObject(new URLAliasRefList(array_merge( $this->urlAliasService->listLocationAliases($data->location, false), $this->urlAliasService->listLocationAliases($data->location, true) ), $this->router->generate( 'ibexa.rest.list_location_url_aliases', ['locationPath' => trim($data->location->pathString, '/')] ))); $generator->endHashElement('Location'); } } ``` This new `ValueObjectVisitor` receives a new tag `app.rest.output.value_object.visitor` to be associated to the new `ValueObjectVisitorDispatcher` in the next step. This tag has a `type` property to associate the new `ValueObjectVisitor` with the type of value is made for. ```yaml services: #… App\Rest\ValueObjectVisitor\RestLocation: class: App\Rest\ValueObjectVisitor\RestLocation parent: Ibexa\Contracts\Rest\Output\ValueObjectVisitor arguments: $urlAliasService: '@ibexa.api.service.url_alias' tags: - { name: app.rest.output.value_object.visitor, type: Ibexa\Rest\Server\Values\RestLocation } ``` ## New `ValueObjectVisitorDispatcher` The new `ValueObjectVisitorDispatcher` receives the `ValueObjectVisitor`s tagged `app.rest.output.value_object.visitor`. As not all value FQCNs are handled, the new `ValueObjectVisitorDispatcher` also receives the default one as a fallback. ```yaml services: #… App\Rest\Output\ValueObjectVisitorDispatcher: class: App\Rest\Output\ValueObjectVisitorDispatcher arguments: - !tagged_iterator { tag: 'app.rest.output.value_object.visitor', index_by: 'type' } - '@Ibexa\Contracts\Rest\Output\ValueObjectVisitorDispatcher' ``` ```php visitors = []; foreach ($visitors as $type => $visitor) { $this->visitors[$type] = $visitor; } } public function setOutputVisitor(Visitor $outputVisitor): void { $this->outputVisitor = $outputVisitor; $this->valueObjectVisitorDispatcher->setOutputVisitor($outputVisitor); } public function setOutputGenerator(Generator $outputGenerator): void { $this->outputGenerator = $outputGenerator; $this->valueObjectVisitorDispatcher->setOutputGenerator($outputGenerator); } public function visit($data) { $className = $data::class; if (isset($this->visitors[$className])) { return $this->visitors[$className]->visit($this->outputVisitor, $this->outputGenerator, $data); } return $this->valueObjectVisitorDispatcher->visit($data); } } ``` ## New `Output\Visitor` service The following new pair of `Ouput\Visitor` entries associates `Accept` headers starting with `application/app.api.` to the new `ValueObjectVisitorDispatcher` for both XML and JSON. A priority is set higher than other `ibexa.rest.output.visitor` tagged built-in services. ```yaml parameters: #… app.rest.output.visitor.xml.regexps: ['(^application/app\.api\.[A-Za-z]+\+xml$)'] app.rest.output.visitor.json.regexps: ['(^application/app\.api\.[A-Za-z]+\+json$)'] services: #… app.rest.output.visitor.xml: class: Ibexa\Contracts\Rest\Output\Visitor arguments: - '@Ibexa\Rest\Output\Generator\Xml' - '@App\Rest\Output\ValueObjectVisitorDispatcher' tags: - { name: ibexa.rest.output.visitor, regexps: app.rest.output.visitor.xml.regexps, priority: 20 } app.rest.output.visitor.json: class: Ibexa\Contracts\Rest\Output\Visitor arguments: - '@Ibexa\Rest\Output\Generator\Json' - '@App\Rest\Output\ValueObjectVisitorDispatcher' tags: - { name: ibexa.rest.output.visitor, regexps: app.rest.output.visitor.json.regexps, priority: 20 } ``` ## Testing the new media-type In the following example, `curl` and `diff` commands are used to compare the default media type (`application/vnd.ibexa.api.Location+xml`) with the new `application/app.api.Location+xml`. ```bash diff --ignore-space-change \ <(curl --silent https://api.example.com/api/ibexa/v2/content/locations/1/2) \ <(curl --silent https://api.example.com/api/ibexa/v2/content/locations/1/2 --header 'Accept: application/app.api.Location+xml'); ``` ```diff 2c2,3 < --- > > 37a39,42 > > > > ``` # Creating new REST resource > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). Extend REST API by creating a new resource. To create a new REST resource, you need to prepare: - the REST route leading to a controller action - the controller and its action - one or several `InputParser` objects if the controller needs to receive a payload to treat, one or several value classes to represent this payload and potentially one or several new media types to type this payload in the `Content-Type` header (optional) - one or several new value classes to represent the controller action result, their `ValueObjectVisitor` to help the generator to turn this into XML or JSON and potentially one or several new media types to claim in the `Accept` header the desired value (optional) - the addition of this resource route to the REST root (optional) In the following example, you add a greeting resource to the REST API. It's available through `GET` and `POST` methods. `GET` sets default values while `POST` allows inputting custom values. ## Route New REST routes should use the [REST URI prefix](https://doc.ibexa.co/en/saas/api/rest_api/rest_api_usage/rest_api_usage/#uri-prefix) for consistency. To ensure that they do, in the `config/routes.yaml` file, while importing a REST routing file, use `ibexa.rest.path_prefix` parameter as a `prefix`. ```yaml app.rest: resource: routes_rest.yaml prefix: '%ibexa.rest.path_prefix%' ``` The `config/routes_rest.yaml` file imported above is created with the following configuration: ```yaml app.rest.greeting: path: '/greet' controller: App\Rest\Controller\DefaultController::helloWorld methods: [GET] ``` ### CSRF protection If a REST route is designed to be used with [unsafe methods](https://doc.ibexa.co/en/saas/api/rest_api/rest_api_usage/rest_requests/#request-method), the CSRF protection is enabled by default like for built-in routes. You can disable it by using the route parameter `csrf_protection`. ```yaml app.rest.greeting: path: '/greet' controller: App\Rest\Controller\DefaultController::helloWorld methods: [GET,POST] defaults: csrf_protection: false ``` ## Controller ### Controller service You can use the following configuration to have all controllers from the `App\Rest\Controller\` namespace (files in the `src/Rest/Controller/` folder) to be set as REST controller services. ```yaml services: #… App\Rest\Controller\: resource: '../src/Rest/Controller/' parent: Ibexa\Rest\Server\Controller autowire: true autoconfigure: true tags: [ 'controller.service_arguments' ] ``` Having the REST controllers set as services enables using features such as the `InputDispatcher` service in the [Controller action](#controller-action). ### Controller action A REST controller should: - return a value object and have a `Generator` and `ValueObjectVisitor`s producing the XML or JSON output - extend `Ibexa\Rest\Server\Controller` to inherit utils methods and properties like `InputDispatcher` or `RequestParser` ```php getMethod()) { return $this->inputDispatcher->parse( new Message( ['Content-Type' => $request->headers->get('Content-Type')], $request->getContent() ) ); } return new Greeting(); } } ``` If the returned value was depending on a location, it could have been wrapped in a `CachedValue` to be cached by the reverse proxy (like Varnish) for future calls. `CachedValue` is used in the following way: ```php use Ibexa\Rest\Server\Values\CachedValue; $locationId = 12345; return new CachedValue( new MyValue($args), ['locationId' => $locationId] ); ``` ## Value and ValueObjectVisitor ```php setHeader('Content-Type', $generator->getMediaType('Greeting')); $generator->startObjectElement('Greeting'); $generator->attribute('href', $this->router->generate('app.rest.greeting')); $generator->valueElement('Salutation', $data->salutation); $generator->valueElement('Recipient', $data->recipient); $generator->valueElement('Sentence', "{$data->salutation} {$data->recipient}"); $generator->endObjectElement('Greeting'); } } ``` The `Values/Greeting` class is linked to its `ValueObjectVisitor` through the service tag. ```yaml services: #… App\Rest\ValueObjectVisitor\Greeting: parent: Ibexa\Contracts\Rest\Output\ValueObjectVisitor tags: - { name: ibexa.rest.output.value_object.visitor, type: App\Rest\Values\Greeting } ``` Here, the media type is `application/vnd.ibexa.api.Greeting` plus a format. To have a different vendor than the default, you could create a new `Output\Generator` or hard-code it in the `ValueObjectVisitor` like in the [`RestLocation` example](https://doc.ibexa.co/en/saas/api/rest_api/extending_rest_api/adding_custom_media_type/#new-restlocation-valueobjectvisitor). ## InputParser A REST resource could use route parameters to handle input, but this example illustrates the usage of an input parser. For this example, the structure is a `GreetingInput` root node with two leaf nodes, `Salutation` and `Recipient`. ```php Good morning'; curl https://api.example.com/api/ibexa/v2/greet --include --request POST \ --header 'Content-Type: application/vnd.ibexa.api.GreetingInput+json' \ --data '{"GreetingInput": {"Salutation": "Good day", "Recipient": "Earth"}}' \ --header 'Accept: application/vnd.ibexa.api.Greeting+json'; ``` ```http HTTP/1.1 200 OK Content-Type: application/vnd.ibexa.api.greeting+xml Hello World Hello World HTTP/1.1 200 OK Content-Type: application/vnd.ibexa.api.greeting+xml Good morning World Good morning World HTTP/1.1 200 OK Content-Type: application/vnd.ibexa.api.greeting+json { "Greeting": { "_media-type": "application\/vnd.ibexa.api.Greeting+json", "_href": "\/api\/ibexa\/v2\/greet", "Salutation": "Good day", "Recipient": "Earth", "Sentence": "Good day Earth" } } ``` ## Registering resources in REST root You can add the new resource to the [root resource](https://doc.ibexa.co/en/saas/api/rest_api/rest_api_usage/rest_api_usage/#rest-root) through a configuration with the following pattern: ```yaml ibexa_rest: system: : rest_root_resources: : mediaType: href: 'router.generate("", {routeParameter: value})' ``` The `router.generate` renders a URI based on the name of the route and its parameters. The parameter values can be a real value or a placeholder. For example, `'router.generate("ibexa.rest.load_location", {locationPath: "1/2"})'` results in `/api/ibexa/v2/content/locations/1/2` while `'router.generate("ibexa.rest.load_location", {locationPath: "{locationPath}"})'` gives `/api/ibexa/v2/content/locations/{locationPath}`. This syntax is based on Symfony's [expression language](https://symfony.com/doc/7.4/expression_language.html), an extensible component that allows limited/readable scripting to be used outside the code context. In this example, `app.rest.greeting` is available in every SiteAccess (`default`): ```yaml ibexa_rest: system: default: rest_root_resources: greeting: mediaType: Greeting href: 'router.generate("app.rest.greeting")' ``` You can place this configuration in any regular config file, like the existing `config/packages/ibexa.yaml`, or a new `config/packages/ibexa_rest.yaml` file. The above example adds the following entry to the root XML output: ```xml ``` # REST API authentication > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). To authenticate REST API communication you can use session (default), JWT, basic, OAuth and client certificate (SSL) authentication. This page refers to [REST API reference](https://doc.ibexa.co/en/saas/api/rest_api/rest_api_reference/rest_api_reference.html), where you can find detailed information about REST API resources and endpoints. Five authentication methods are currently supported: session (default), JWT, basic, OAuth, and client certificate (SSL). You can only use one of those methods at the same time. Using HTTPS for authenticated traffic is highly recommended. For other security related subjects, see: - [Cross-origin requests](https://doc.ibexa.co/en/saas/api/rest_api/rest_api_usage/rest_responses/#cross-origin) - [`access_control`](https://symfony.com/doc/7.4/security/access_control.html) > **Caution: SiteAccess login** > > The anonymous user is used to perform authentification requests. Therefore, the "Anonymous" role must have `user/login` permission on the SiteAccess that matches the REST domain or is passed through the [`X-Siteaccess` header](https://doc.ibexa.co/en/saas/api/rest_api/rest_api_usage/rest_requests/#siteaccess). ## Session-based authentication This authentication method requires a session cookie to be sent with each request. If you use this authentication method with a web browser, this session cookie is automatically available as soon as your visitor logs in. Add it as a cookie to your REST requests to authenticate the user. Sessions are created to re-authenticate the user only (and perform authorization), not to hold session state in the service. Because of that, you can use this method as supporting AJAX-based applications even if it violates the principles of RESTful services. ### Configuration Session is the default method and is already enabled, so no configuration required. Enabling any other method disables session. ### Usage examples You can create a session for a visitor even if they're not logged in by sending the **`POST`** request to `/user/sessions`. To log out, use the **`DELETE`** request on the same resource. #### Establishing session ##### Creating session To create a session, execute the following REST request: **XML** ```http POST /user/sessions HTTP/1.1 Host: www.example.net Accept: application/vnd.ibexa.api.Session+xml Content-Type: application/vnd.ibexa.api.SessionInput+xml ``` ```xml admin publish ``` ```http HTTP/1.1 201 Created Location: /user/sessions/go327ij2cirpo59pb6rrv2a4el2 Set-Cookie: IBX_SESSION_ID98defd6ee70dfb1dea416=go327ij2cirpo59pb6rrv2a4el2; domain=.example.net; path=/; expires=Wed, 13-Jan-2021 22:23:01 GMT; HttpOnly Content-Type: application/vnd.ibexa.api.Session+xml ``` ```xml IBX_SESSION_ID98defd6ee70dfb1dea416 go327ij2cirpo59pb6rrv2a4el2 23lk.neri34ijajedfw39orj-3j93 ``` **JSON** ```http POST /user/sessions HTTP/1.1 Host: www.example.net Accept: application/vnd.ibexa.api.Session+json Content-Type: application/vnd.ibexa.api.SessionInput+json ``` ```json { "SessionInput": { "login": "admin", "password": "publish" } } ``` ```http HTTP/1.1 201 Created Location: /user/sessions/go327ij2cirpo59pb6rrv2a4el2 Set-Cookie: IBX_SESSION_ID98defd6ee70dfb1dea416=go327ij2cirpo59pb6rrv2a4el2; domain=.example.net; path=/; expires=Wed, 13-Jan-2021 22:23:01 GMT; HttpOnly Content-Type: application/vnd.ibexa.api.Session+xml ``` ```json { "Session": { "_media-type": "application\/vnd.ibexa.api.Session+json", "_href": "\/api\/ibexa\/v2\/user\/sessions\/jg1nhinvepsb9ivd10hbjbdp4l", "name": "IBX_SESSION_ID98defd6ee70dfb1dea416", "identifier": "go327ij2cirpo59pb6rrv2a4el2", "csrfToken": "23lk.neri34ijajedfw39orj-3j93", "User": { "_media-type": "application\/vnd.ibexa.api.User+json", "_href": "\/api\/ibexa\/v2\/user\/users\/14" } } } ``` ##### Logging in with active session Logging in is similar to session creation, with one important detail: the CSRF token obtained in the previous step is added to the new request through the `X-CSRF-Token` header. **XML** ```http POST /user/sessions HTTP/1.1 Host: www.example.net Accept: application/vnd.ibexa.api.Session+xml Content-Type: application/vnd.ibexa.api.SessionInput+xml Cookie: IBX_SESSION_ID98defd6ee70dfb1dea416=go327ij2cirpo59pb6rrv2a4el2 X-CSRF-Token: 23lk.neri34ijajedfw39orj-3j93 ``` ```xml admin publish ``` ```http HTTP/1.1 200 OK Content-Type: application/vnd.ibexa.api.Session+xml ``` ```xml IBX_SESSION_ID98defd6ee70dfb1dea416 go327ij2cirpo59pb6rrv2a4el2 23lk.neri34ijajedfw39orj-3j93 ``` **JSON** ```http POST /user/sessions HTTP/1.1 Host: www.example.net Accept: application/vnd.ibexa.api.Session+json Content-Type: application/vnd.ibexa.api.SessionInput+json Cookie: IBX_SESSION_ID98defd6ee70dfb1dea416=go327ij2cirpo59pb6rrv2a4el2 X-CSRF-Token: 23lk.neri34ijajedfw39orj-3j93 ``` ```xml { "SessionInput": { "login": "admin", "password": "publish" } } ``` ```http HTTP/1.1 200 OK Content-Type: application/vnd.ibexa.api.Session+json ``` ```xml { "Session": { "_media-type": "application\/vnd.ibexa.api.Session+json", "_href": "\/api\/ibexa\/v2\/user\/sessions\/jg1nhinvepsb9ivd10hbjbdp4l", "name": "IBX_SESSION_ID98defd6ee70dfb1dea416", "identifier": "go327ij2cirpo59pb6rrv2a4el2", "csrfToken": "23lk.neri34ijajedfw39orj-3j93", "User": { "_media-type": "application\/vnd.ibexa.api.User+json", "_href": "\/api\/ibexa\/v2\/user\/users\/14" } } } ``` #### Using session ##### Session cookie You can now add the previously set cookie to requests to be executed with the logged-in user. ```http GET /content/locations/1/5 HTTP/1.1 Host: www.example.net Accept: Accept: application/vnd.ibexa.api.Location+xml Cookie: IBX_SESSION_ID98defd6ee70dfb1dea416=go327ij2cirpo59pb6rrv2a4el2 ``` ##### CSRF token It can be important to keep the CSRF token (`csrfToken`) for the duration of the session, because you must send this token in every request that uses [unsafe HTTP methods](https://doc.ibexa.co/en/saas/api/rest_api/rest_api_usage/rest_requests/#request-method) (others than the safe GET or HEAD or OPTIONS) when a session has been established. It should be sent with an `X-CSRF-Token` header. Only three built-in routes can accept unsafe methods without CSRF, the sessions routes starting with `/user/sessions` to create, refresh or delete a session. ```http DELETE /content/types/32 HTTP/1.1 Host: www.example.net Cookie: IBX_SESSION_ID98defd6ee70dfb1dea416=go327ij2cirpo59pb6rrv2a4el2 X-CSRF-Token: 23lk.neri34ijajedfw39orj-3j93 ``` If an unsafe request is missing the CSRF token, or the token has incorrect value, an error is returned: `401 Unauthorized`. ##### Rich client application security concerns The purpose of CSRF protection is to prevent users from accidentally running harmful operations by being tricked into executing an HTTP(S) request against a web applications they're logged into. In browsers this action is blocked by lack of CSRF token. However, if you develop a rich client application (for example, JavaScript, JAVA, iOS, or Android), that is: - Registering itself as a protocol handler: - Exposes unsafe methods in any way - Authenticates using either: - Session-based authentication - "Client side session" by remembering user login/password Then, you have to make sure to confirm with the user if they want to perform an unsafe operation. Example: A rich JavaScript/web application uses `navigator.registerProtocolHandler()` to register "web+ez:" links to go against REST API. It uses a session-based authentication, and it's in widespread use across the net, or/and it's used by everyone within a company. A person with minimal insight into this application and the company can easily send out the following link to all employees in that company in email: `latest reports`. #### Logging out from session To log out is to `DELETE` the session using its ID (like in the cookie). As this is an unsafe method, the CSRF token must be presented. ```http DELETE /user/sessions/go327ij2cirpo59pb6rrv2a4el2 HTTP/1.1 Host: www.example.net Cookie: IBX_SESSION_ID98defd6ee70dfb1dea416=go327ij2cirpo59pb6rrv2a4el2 X-CSRF-Token: 23lk.neri34ijajedfw39orj-3j93 ``` ## JWT authentication ### Configuration See [JWT authentication](https://doc.ibexa.co/en/saas/infrastructure_and_maintenance/security/development_security/#jwt-authentication) for configuration instructions. ### Usage example After you configure JWT authentication for REST, you can get the JWT token through the following request: ```http POST /user/token/jwt HTTP/1.1 Host: Accept: application/vnd.ibexa.api.JWT+json Content-Type: application/vnd.ibexa.api.JWTInput+json ``` Provide the username and password in the request body: ```json { "JWTInput": { "username": "admin", "password": "publish" } } ``` If credentials are valid, the server response contains a token: ```json { "JWT": { "_media-type": "application/vnd.ibexa.api.JWT+xml", "_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9…-QBE4-6eKNjg" } } ``` You can then use this token in your request instead of username and password. ```http GET /content/locations/1/5/children Host: Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9…-QBE4-6eKNjg Accept: application/vnd.ibexa.api.LocationList+json ``` #### JWT token obtained through REST documentation To obtain a JWT token with REST, you can use the live API documentation that is available on your development installation. This documentation is only accessible when `kernel.debug` is set to `true`, similarly to a development environment. - open REST API live doc (for example at `http://localhost/api/ibexa/v2/doc`) - go to **User Token** section's **POST /user/token/jwt** resource (for example, at `http://localhost/api/ibexa/v2/doc#/User%20Token/api_usertokenjwt_post`) - click the **Try it out** button - fill in the following adapted payload with the user credentials - click the **Execute** button to get a token ![REST API live documentation with a JWTInput payload](https://doc.ibexa.co/en/saas/api/rest_api/img/jwt-rest-doc-request.png "REST doc JWT token request") ![REST API live documentation with a JWTInput payload](https://doc.ibexa.co/en/saas/api/rest_api/img/jwt-rest-doc-response.png "REST doc JWT token response") ## HTTP basic authentication For more information, see [HTTP Authentication: Basic and Digest Access Authentication](https://datatracker.ietf.org/doc/html/rfc2617). ### Configuration If the installation has a dedicated host for REST, you can enable HTTP basic authentication only on this host by setting a firewall like in the following example before the `ibexa_front` one: ```yaml security: firewalls: # ... ibexa_rest: host: ^api\.example\.com$ http_basic: realm: Cohesivo REST API #ibexa_front: # ... ``` > **Caution: Back office uses REST API** > > Back office uses the REST API too (for some parts like the Location tree or the Calendar) on its own domain. > > - If the back office SiteAccess matches `//admin.example.com` (through `Map\Host`, `HostElement` or `HostText`), it calls the REST API under `//admin.example.com/api/ibexa/v2`; > - If the back office SiteAccess matches `//localhost/admin` (through `URIElement`, `Map\URI` or `Regex\URI`), it calls the REST API under `//localhost/api/ibexa/v2` because SiteAccess matching with REST isn't enabled at URL level. > > If you enable basic authentication for `pattern: ^/api/ibexa/v2` to use it in your front office across both production and development environments, your development environment's back office cannot work correctly. This back office tries to access REST through the same URL as the front office. Even when logged in back office and using the [X-SiteAccess header](https://doc.ibexa.co/en/saas/api/rest_api/rest_api_usage/rest_requests/#siteaccess), the firewall blocks access to REST as you're not logged through basic authentification. Therefore, some back office features don't work. > > If basic authentication is used only for REST API, it's better to have a dedicated domain even on a development environment. For example, map an `api.localhost` in your `hosts` file and set the firewall for `host: ^api\.(example\.com|localhost)$`. ### Usage example Basic authentication requires the username and password to be sent *(username:password)*, base64 encoded, with each request. For details, see [RFC 2617](https://datatracker.ietf.org/doc/html/rfc2617). Most HTTP client libraries and REST libraries support this method. [Creating content with binary attachments](https://doc.ibexa.co/en/saas/api/rest_api/rest_api_usage/rest_requests/#creating-content-with-binary-attachments) is an example of using basic authentication with [cURL](https://www.php.net/manual/en/book.curl.php) and its `CURLOPT_USERPWD`. See the following raw HTTP request with basic authentication example: ```http GET / HTTP/1.1 Host: api.example.com Accept: application/vnd.ibexa.api.Root+json Authorization: Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ== ``` ## OAuth For more information, see [OAuth 2.0 protocol for authorization](https://oauth.net/2/). ## SSL client authentication The REST API provides authentication of a user by a subject in a client certificate delivered by the web server configured as SSL endpoint. # GraphQL > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). GraphQL enables making concise, readable requests to Cohesivo APIs. [GraphQL](https://graphql.org/) is a query language for the API. The GraphQL implementation for Cohesivo is located in [`ibexa/graphql`](https://github.com/ibexa/graphql). ## Setup Using GraphQL requires a domain schema. Before using GraphQL for the first time, or anytime you modify content types or product types in your installation, you need to generate the schema: ```bash php bin/console ibexa:graphql:generate-schema php bin/console cache:clear ``` YAML files with the schema are located in `config/graphql/types/ibexa`. They contain information about the domain objects and the fields you can [query](https://doc.ibexa.co/en/saas/api/graphql/graphql_queries/index.md) and [operate on](https://doc.ibexa.co/en/saas/api/graphql/graphql_operations/index.md). ### Schema generation limitations GraphQL schema cannot be generated for names that don't follow the [GraphQL specification](http://spec.graphql.org/June2018/#sec-Names), for example names that start with a digit. This concerns image variations, content types, content type groups, product types, and field definition identifiers. It's recommended to rename the relevant identifiers. Failure to generate schema is registered in logs. To find identifiers that aren't included in the schema, look for "Skipped schema generation" log messages, for example: `Skipped schema generation for Image Variation`. ## Domain schema GraphQL for Cohesivo is based on the content types (including product types), content type groups, and content items defined in the repository. For each content type the schema exposes a singular and plural field, for example, `article` and `articles`. Use the singular field to query a single content item, and the plural to get a whole `Connection` (a list of content items that supports pagination). With the queries you can inspect: - the existing types - details of content types, and their fields in the context of developing your own application You can request additional content information such as the section or Objects states, available under the `_info` field. You can also query content type and content type group information through the `_info` and `_types` fields. ### Repository schema The repository schema, accessed through `_repository`, exposes the Cohesivo repository in a manner similar to the [Public PHP API](https://doc.ibexa.co/en/saas/api/php_api/php_api/index.md). The `_repository` field also enables you to query, for example, object states configured for the repository. ### Custom schemas You can also use your own [custom schema](https://doc.ibexa.co/en/saas/api/graphql/graphql_customization/#custom-schema). ### SiteAccesses and multiple Repositories GraphQL is SiteAccess-aware, but can have only one schema per installation. This means you cannot use GraphQL with multiple repositories. When you request a URL from a SiteAccess that is different than the current one, the API generates it for the content item's SiteAccess, with an absolute URL if necessary. ## Authentication GraphQL for Cohesivo supports session-based authentication. You can get your session cookie by logging in through the interface or through a REST request. ### JWT authentication If you have [JWT authentication](https://doc.ibexa.co/en/saas/infrastructure_and_maintenance/security/development_security/#jwt-authentication) enabled, you can use the following query to get your authentication token: ```graphql mutation CreateToken { createToken(username: "admin", password: "publish") { token message } } ``` Response: ```json { "data": { "createToken": { "token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpYXQiOjE2MDI4MzU5MTksImV4cCI6MTYwMjgzOTUxOSwicm9sZXMiOlsiUk9MRV9VU0VSIl0sInVzZXJuYW1lIjoiYWRtaW4ifQ.QtDjPU6q68fdvgm6O_1-aEoe-s7s-VQr-9CTMC9ba6E", "message": null } } } ``` #### JWT token obtained through GraphiQL To obtain a JWT token, you can use the GraphiQL interface on your development installation. GraphiQL is only accessible when `kernel.debug` is set to `true`, similarly to a development environment. - open GraphiQL UI (for example, at `http://localhost/graphiql`) - paste in the following adapted query with the user credentials - click the execute button **▶** to get a token ```graphql mutation CreateToken { createToken(username: "ibexa-example", password: "Ibexa-3xample") { token message } } ``` ![GraphiQL with a JWT token request and its response](https://doc.ibexa.co/en/saas/ai/mcp/img/jwt-graphiql.png "GraphiQL JWT token request and response") ## Usage You can access GraphQL with `/graphql`. ### GraphiQL client The [GraphiQL interactive client](https://github.com/graphql/graphiql) is included in the installation. Access it through `/graphiql`. Here you can run your queries and preview the results in a readable format. ### Reference GraphiQL offers side-by-side reference based on your generated schema in the **Docs** pane. # GraphQL queries > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). Use GraphQL to query for content and Locations. ## Querying content You can query a single content item or a list of content items using fields defined in the domain schema. ### Get a content item To get a specific content item by its content ID, location ID, or URL alias, use its relevant singular field, for example `article`, `folder`, or `image`. ```graphql { content { article(contentId: 62) { title author { name } } } } ``` Response: ```json { "data": { "content": { "article": { "title": "Travel literature, how to get started", "author": [ { "name": "Administrator User" } ] } } } } ``` You can request any fields of the content item. In the example above, these are `title` and `author`. You can also query the generic `item` object. The `item` object references a content item, but you can also get its [location information](#querying-locations). The query accepts `locationId`, `remoteId`, and `urlAlias` as arguments. ```graphql { item(locationId: 2) { _name ... on FolderItem { name } ... on LandingPageItem { name } ... on ArticleItem { title } } } ``` Response: ```json { "data": { "item": { "_name": "Ibexa Digital Experience Platform" } } } ``` #### Get language versions To get fields of a content item in a specific language, use the `language` argument. The language must be configured for the current SiteAccess. ```graphql { content { article(id: 57) { title: title(language: eng_GB) title_PL: title(language: pol_PL) } } } ``` Response: ```json { "data": { "content": { "article": { "title": "Most interesting cat breeds", "title_PL": "Najciekawsze rasy kotów" } } } } ``` When you don't specify a language, the response contains the most prioritized translation. ### Get a group of content items To get a list of all content items of a selected type, use the plural field, for example, `articles`: ```graphql { content { articles { edges { node { _location { id } title author { name } } } } } } ``` Response: ```json { "data": { "content": { "articles": { "edges": [ { "node": { "_location": { "id": 57 }, "title": "Travel literature, How to get started", "author": [ { "name": "Administrator User" } ] } }, { "node": { "_location": { "id": 58 }, "title": "Why we love NYC", "author": [ { "name": "Administrator User" } ] } }, # ... ] } } } } ``` > **Tip: Edges** > > `edges` are used when querying plural fields to offer [pagination](#pagination). ### Get content type information To get the IDs and names of all Fields in the `article` content type: ```graphql { content { _types { article { _info { fieldDefinitions { id name } } } } } } ``` Response: ```json { "data": { "content": { "_types": { "article": { "_info": { "fieldDefinitions": [ { "id": 1, "name": "Title" }, { "id": 152, "name": "Short title" }, { "id": 153, "name": "Author" }, { "id": 120, "name": "Intro" }, { "id": 121, "name": "Body" }, { "id": 123, "name": "Enable comments" }, { "id": 154, "name": "Image" } ] } } } } } } ``` ## Querying Locations You can get the Location object from any item by querying for `_location` or `_allLocations`. When you use `_location`, the API returns: - the location specified in the `locationId` or `urlAlias` argument - the location based on the current SiteAccess - the main location ```graphql { content { folder (contentId: 133) { _allLocations { pathString } } } } ``` Response: ```json { "data": { "content": { "folder": { "_allLocations": [ { "pathString": "/1/2/128/132/" }, { "pathString": "/1/2/133/" } ] } } } } ``` To query the URL alias of a content item, use `_url`. This returns the "best" URL alias for this content item based on its main Location and the current SiteAccess: ```graphql { content { folder (contentId: 1) { _url } } } ``` Response: ```json { "data": { "content": { "folder": { "_url": "/site/ez-platform" } } } } ``` ## Getting children of a Location To get a [location's](#querying-locations) children, it's recommended to use the [Query field](https://doc.ibexa.co/en/saas/templating/queries_and_controllers/content_queries/#content-query-field). Alternatively, you can query the `children` property of an `item` or `content` object: ```graphql { item(locationId: 2) { _location { children { edges { node { content { _name _type { name } } } } } } } } ``` Response: ```json { "data": { "item": { "_location": { "children": { "edges": [ { "node": { "content": { "_name": "Ibexa Platform", "_type": { "name": "Folder" } } } }, { "node": { "content": { "_name": "Product Catalog", "_type": { "name": "Product catalog" } } } } ] } } } } ``` ## Querying products You can query a single product, products of one type, or all products by providing criteria. > **Note: Note** > > GraphQL schema for product catalog is generated only when at least one product type exists in the system. If your queries fail, make sure you regenerated the schema. To get a single product by its code: ```graphql { products { single(code: "DRESUN") { name productType {name} createdAt { timestamp } } } } ``` Response: ```json { "data": { "products": { "single": { "name": "Sundress", "productType": { "name": "Dress" }, "createdAt": { "timestamp": 1649229733 } } } } } ``` To get products of a specific type: ```graphql { products { byType { dresses { edges { node { name code } } } } } } ``` Response: ```json { "data": { "products": { "byType": { "dresses": { "edges": [ { "node": { "name": "Sundress", "code": "DRESUN" } }, { "node": { "name": "Cocktail dress", "code": "DRECO" } } ] } }, } } } ``` To get all products, using specific criteria (in this case, unavailable products): ```graphql { products { all( availability:unavailable sortBy: [name] ) { edges { node { name code } } } } } ``` Response: ```json { "data": { "products": { "all": { "edges": [ { "node": { "name": "Cocktail dress", "code": "DRECO" } }, { "node": { "name": "Sundress", "code": "DRESUN" } } ] } } } } ``` ## Filtering To get all articles with a specific text: ```graphql { content { articles(query: {Text:"travel"}) { edges { node { title } } } } } ``` Response: ```json { "data": { "content": { "articles": { "edges": [ { "node": { "title": "Travel literature, How to get started" } }, { "node": { "title": "Travel with your dog" } } ] } } } } ``` To filter products based on content fields: ```graphql { products { all { edges { node { fields { ... on DressContentFields { name description { plaintext } } _all { fieldDefIdentifier value } } } } } } } ``` ### Querying product attributes To filter products based on attributes: ```graphql { products { single(code: "BLUELACE") { attributes { ... on DressAttributes { measure { reason } lace_color { identifier value } } } } } } ``` If the attribute type (in this case, `measure`) cannot be found in the schema, the response is: ```json { "data": { "products": { "single": { "attributes": { "measure": { "reason": "This attribute type isn't yet part of the schema." }, "lace_color": { "identifier": "lace_color", "value": "#387be8" } } } } } } ``` You can also query attributes by providing the attribute type: ```graphql { products { all { edges { node { attributes { _all { ... on ColorAttribute { identifier name colorValue: value } ... on IntegerAttribute { identifier name sizeValue: value } } } } } } } } ``` > **Note: Note** > > You need to use aliases (for example, `sizeValue`) when querying attributes by the attribute type due to the conflicting return types. Response: ```json { "data": { "products": { "all": { "edges": [ { "node": { "attributes": { "_all": [ { "identifier": "size", "name": "Size", "sizeValue": 36 }, { "identifier": "color", "name": "Color", "colorValue": "#fcff38" } ] } } }, { "node": { "attributes": { "_all": [ { "identifier": "color", "name": "Color", "colorValue": "#000000" }, { "identifier": "size", "name": "Size", "sizeValue": 40 } ] } } } ] } } } } ``` ## Sorting You can sort query results using `sortBy`: ```graphql { content { articles(sortBy: _datePublished) { edges { node { title } } } } } ``` You can use an array of clauses as well. To reverse the item list, add `_desc` after the clause: ```graphql articles(sortBy:[_datePublished,_desc]) ``` ## Pagination GraphQL offers [cursor-based pagination](https://graphql.org/learn/pagination/) for paginating query results. You can paginate plural fields by using `edges`: ```graphql { content { articles(sortBy: _datePublished, first:3) { pageInfo { hasNextPage endCursor } edges { node { title } } } } } ``` This query returns the first three articles, ordered by their publication date. If the current `Connection` (list of results) isn't finished yet and there are more items to read, `hasNextPage` is `true`. For the `children` node, you can use the following pagination method: ```graphql { _repository { location(locationId: 2) { children(first: 3) { pages { number cursor } edges { node { content { _name } } } } } } } ``` Response: ```json { "data": { "_repository": { "location": { "children": { "pages": [ { "number": 2, "cursor": "YXJyYXljb25uZWN0aW9uOjE=" }, { "number": 3, "cursor": "YXJyYXljb25uZWN0aW9uOjM=" } ], "edges": [ { # ... } ] } } } } } ``` In the response, `number` contains page numbers, starting with 2 (because 1 is the default). To request a specific page, provide the `cursor` as an argument to `children`: ```graphql children(first: 3, after: "YXJyYXljb25uZWN0aW9uOjM=") ``` ### Get Matrix field type To get a Matrix field type with GraphQL, see [Matrix field type reference](https://doc.ibexa.co/en/saas/content_management/field_types/field_type_reference/matrixfield/index.md). # GraphQL operations > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). Use GraphQL operations to create, update, and delete content. Operations on content in GraphQL are performed by using [mutations](https://graphql.org/learn/mutations/). They include creating, updating, and deleting content items. The schema contains two mutations per content type, for example, `createFolder`, and `updateFolder`. You can also make use of the generic `deleteContent` and `uploadFiles` mutations. ## Creating content Create a new Folder as a child of Location `2` with: ```graphql mutation createFolder { createFolder( language: eng_GB parentLocationId: 2 input: { name: "New Folder" } ) { id } } ``` Response: ```json { "data": { "createFolder": { "id": "RG9tYWluQ29udGVudDo2NA==" } } } ``` ## Updating content Modify the name of a Folder content item with: ```graphql mutation updateFolder { updateFolder( language: eng_GB contentId: 64 input: { name: "New Folder name" } ) { id } } ``` Response: ```json { "data": { "updateFolder": { "id": "RG9tYWluQ29udGVudDo2NA==" } } } ``` The input for updating a content item is the same as when creating it, but all fields are optional. ## Deleting content You can delete any content item by providing its `contentId` (or its GraphQL opaque ID under `id`): ```graphql mutation deleteBlogPost { deleteContent(contentId: 64) { id contentId } } ``` Response: ```json { "data": { "deleteContent": { "id": "Rm9sZGVyQ29udGVudDo2NA==", "contentId": 64 } } } ``` ## File upload > **Note: Note** > > Uploading binary files isn't possible through GraphiQL. You can use alternative third-party clients such as [Altair GraphQL](https://altairgraphql.dev/). Uploading files makes use of dedicated mutations per content type, for example: ```graphql mutation CreateImage($file: FileUpload!) { createImage( parentLocationId: 51, language: eng_GB, input: { name: "An image created over GraphQL", image: { alternativeText: "The alternative text", file: $file } } ) { _info { id mainLocationId } name image { fileName alternativeText uri } } } ``` The file is provided as the `$file` variable, defined as an `UploadFile`. You can include this mutation in a cURL request under `operations`: ```bash curl -v -X POST \ /graphql \ -H "Cookie: $AUTH_COOKIE" \ -F 'operations={"query":"mutation createFile($file: FileUpload!) { ... }","variables":{"file": null}}' \ -F 'map={"image":["variables.file"]}' \ -F "image"=@/path/to/image.png ``` For example: ```bash curl -v -X POST \ /graphql \ -H "Cookie: $AUTH_COOKIE" \ -F 'operations={"query":"mutation CreateImage($file: FileUpload!) { createImage( parentLocationId: 51, input: { name: \"An image created over GraphQL\", image: { alternativeText: \"The alternative text\", file: $file } }, language: \"eng-GB\" ) { _info { id mainLocationId } _url name image { fileName alternativeText uri } } }","variables":{"file": null}}' \ -F 'map={"image":["variables.file"]}' \ -F "image"=@/path/to/image.png ``` > **Note: Authentication** > > The example above requires you to set your authentication cookie in the `$AUTH_COOKIE` variable. For more information, see [Authentication](https://doc.ibexa.co/en/saas/api/graphql/graphql/#authentication). ### Uploading multiple files You can upload multiple files with one operation in a similar way by using the `uploadFiles` mutation. Here the files are provided in a `$files` variable and listed under `map` in the cURL request. ```graphql mutation UploadMultipleFiles($files: [FileUpload]!) { uploadFiles( locationId: 51, files: $files, language: eng_GB ) { files { _url _location { id } ... on ImageContent { name image { uri } } ... on FileContent { name file { uri } } ... on VideoContent { name file { uri } } } warnings } } ``` Include this mutation in a cURL request: ```bash curl -v -X POST \ /graphql \ -H 'Cookie: $AUTH_COOKIE' \ -F 'operations={"query": "mutation UploadMultipleFiles($files: [FileUpload]!) { uploadFiles( locationId: 51, files: $files, languageCode: \"eng-GB\" ) { files { _url _location { id } ... on ImageContent { name image { uri } } ... on FileContent { name file { uri } } ... on VideoContent { name file { uri } } } warnings } }", "variables": {"files": [null, null, null, null, null]}}' \ -F 'map={"image1":["variables.files.0"], "image2":["variables.files.1"], "file1":["variables.files.2"], "file2":["variables.files.3"], "media":["variables.files.4"]}' \ -F "image1"=@/tmp/files/image1.png \ -F "image2"=@/tmp/files/image2.png \ -F "file1"=@/tmp/files/file1.pdf \ -F "file2"=@/tmp/files/file2.zip \ -F "media"=@/tmp/files/media.mp4 ``` # GraphQL customization > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). Customize your GraphQL API with a custom schema. ## Custom schema You can customize the GraphQL schema that is generated from your repository. You can use it if your application requires custom GraphQL resources, for instance for Doctrine entities. To do so, create a `config/graphql/types/Query.types.yaml` file. It is used as the GraphQL query root. In that file, add new fields that use any custom type or custom logic you require, based on [overblog/GraphQLBundle](https://github.com/overblog/GraphQLBundle). The custom schema should be created only after generating other schemas to avoid problems, especially if the custom schema depends on other schema elements. For example, `Type "Domain" inherited by "Query" not found.`. To avoid this problem during deployment, add the generated schemas to the repository. Update the schema in the event of any changes related to GraphQL and when changing the environment, for example from `dev` to `prod`. ### Configuration You can include the Cohesivo schema in two ways: either through inheritance or composition. #### Inheritance To use inheritance, apply the following configuration in `config/graphql/types/Query.types.yaml`: ```yaml Query: type: object inherits: - Domain config: fields: customField: type: object ``` #### Composition To use composition, define Cohesivo schema as a field in your custom schema. For example, in `config/graphql/types/Query.types.yaml`: ```yaml Query: type: object config: fields: myCustomField: {} myOtherCustomField: {} ibexa: type: Domain ``` ### Custom mutations Custom mutations are created in the same way as custom query configuration. A `config/graphql/types/Mutation.types.yaml` file is used as the source for mutation definitions in your schema. ```yaml Mutation: type: object inherits: [PlatformMutation] config: fields: createSomething: builder: Mutation builderConfig: inputType: CreateSomethingInput payloadType: SomethingPayload mutateAndGetPayload: "@=mutation('CreateSomething', [value])" CreateSomethingInput: type: relay-mutation-input config: fields: name: type: String SomethingPayload: type: object config: fields: name: type: String ``` ## Custom field name You can customize the name used by GraphQL as the content field name. Use this setting to avoid conflicts with field names that derive from a content type definition. ```yaml parameters: ibexa_graphql.schema.content.field_name.override: id: id_ ``` # Add GraphQL support to custom field types > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). Add GraphQL support to custom field types. If you want to use custom field types in GraphQL, you need to map them. Their values and field definition structure, need to be defined, to interact with them using GraphQL. For example: | Name | Possible field value | Resolver | Field definition | | ------------- | -------------------------------- | ---------- | ---------------------------- | | Text Line | string | default | `TextLineFieldDefinition` | | Relation List | `Item` `ArticleItem` `ImageItem` | customized | `RelationListFieldDefinitio` | ## Map a custom field type There are two ways of mapping a custom field type: - configuration - custom `FieldDefinitionMapper` You need to write a custom `FieldDefinitionMapper` if the field definition settings and constraints impact how it's mapped to GraphQL. For example, the selection field type has a "multiple" option. If set to false, it accepts and returns a single value, but if set to true, it accepts and returns an array of values. If your field definition doesn't require additional clarifications, you can map it with configuration. ### Map with configuration To map a custom field type with configuration use a compiler pass to modify a container parameter, `ibexa.graphql.schema.content.mapping.field_definition_type`. It's a hash that maps a field type identifier (`ibexa_string`) to the following entries: - `value_type` - the GraphQL type values of the custom field. It can be a native type (string, int), or a custom type. If none is specified, string is used. - `value_resolver` - how values of this field are resolved and passed to the defined value type. If not specified, it receives the `Field` object for the field type: `field`. - `definition_type` - the GraphQL type the field definitions is mapped to. If not specified, it uses `FieldDefinition`. Compiler pass example that should be placed in `src/DependencyInjection/Compiler`: ```php hasParameter('ibexa.graphql.schema.content.mapping.field_definition_type')) { return; } $mapping = $container->getParameter('ibexa.graphql.schema.content.mapping.field_definition_type'); $mapping['my_custom_fieldtype'] = [ 'value_type' => 'MyCustomFieldValue', 'definition_type' => 'MyCustomFieldDefinition', 'value_resolver' => 'field.someProperty', ]; } } ``` ### Map with a custom `FieldDefinitionMapper` The `FieldDefinitionMapper` API uses service decorators. To register your own mapper, make it decorate the `Ibexa\GraphQL\Schema\Domain\Content\Mapper\FieldDefinition\DecoratingFieldDefinitionMapper` service: ```yaml services: App\GraphQL\Schema\MyFieldDefinitionMapper: decorates: Ibexa\GraphQL\Schema\Domain\Content\Mapper\FieldDefinition\DecoratingFieldDefinitionMapper arguments: $innerMapper: '@.inner' ``` The `$innerMapper` argument passes the decorated mapper to the constructor. You can use the `DecoratingFieldDefinitionMapper` from the `graphql` package. It requires that you implement the `getFieldTypeIdentifier` method to tell which field type is covered by the mapper. Add `MyFieldDefinitionMapper.php` mapper to `src/GraphQL/Schema`: ```php canMap($fieldDefinition)) { return parent::mapToFieldValueInputType($contentType, $fieldDefinition); } ``` It's required for every implemented method, so that other mappers are called for the other field types. For an example implementation, look at the [`RelationFieldDefinitionMapper`](https://github.com/ibexa/graphql/blob/6.0/src/lib/Schema/Domain/Content/Mapper/FieldDefinition/RelationFieldDefinitionMapper.php) class. The value type depends on the field definition allowed content types setting: - for types that return content items if there are no restrictions, or several types are allowed, the value is an `Item` The cardinality (single or collection) depends on the selection limit setting: - if only one item is allowed, the value is unique: `ArticleItem`, `FolderItem` - if there are no limits, or the limit is larger than 1, the value is a collection: `"[ArticleItem]", "[FolderItem]"`. #### Field input mapping The `mapToFieldValueInputType` method is used to document what input type is expected by field types that require a more complex input value. For example, `ibexa_matrix` generates its own input types depending on the configured columns. Example of a `MyFieldDefinitionMapper` mapper for a complex field type: ```php canMap($fieldDefinition)) { return parent::mapToFieldValueInputType($contentType, $fieldDefinition); } return $this->nameMyFieldInputType($contentType, $fieldDefinition); } private function nameMyFieldInputType(ContentType $contentType, FieldDefinition $fieldDefinition): string { $converter = new CamelCaseToSnakeCaseNameConverter(null, false); return sprintf( '%s%sInput', $converter->denormalize($contentType->identifier), $converter->denormalize($fieldDefinition->identifier) ); } } ``` ## Resolver expressions The following variables are available in the resolver's expression: - `field` is the current field, as an extension of the API's field object that proxies properties requests to the field Value - `content` is the resolved content item's `Content` - `location` is the content item's resolved location. For more information, see [Querying Locations](https://doc.ibexa.co/en/saas/api/graphql/graphql_queries/#querying-locations) - `item` is the content together with its location `\Ibexa\GraphQL\Value\Item` `RelationFieldValueBuilder` or `SelectionFieldValueBuilder` can be used as examples. # Event reference > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). Cohesivo dispatches events before and after you perform different operations in the back office and on the Repository. Cohesivo dispatches events during different actions. You can subscribe to these events to extend the functionality of the system. In most cases, two events are dispatched for every action, one before the action is completed, and one after. For example, copying a content item is connected with two events: `BeforeCopyContentEvent` and `CopyContentEvent`. ```php ['onCopyContent', 0], ]; } public function onCopyContent(CopyContentEvent $event): void { // your implementation } } ``` - [AI Actions events](https://ez-systems-developer-documentation--3381.com.readthedocs.build/en/3381/api/event_reference/ai_action_events/): Events that are triggered when working with AI actions. - [Content events](https://ez-systems-developer-documentation--3381.com.readthedocs.build/en/3381/api/event_reference/content_events/): Events that are triggered when working with content. - [Content type events](https://ez-systems-developer-documentation--3381.com.readthedocs.build/en/3381/api/event_reference/content_type_events/): Events that are triggered when working with content types. - [Integrated help events](https://ez-systems-developer-documentation--3381.com.readthedocs.build/en/3381/api/event_reference/integrated_help_events/): Events that are triggered when working with integrated help features like product tours. - [Language events](https://ez-systems-developer-documentation--3381.com.readthedocs.build/en/3381/api/event_reference/language_events/): Events that are triggered when working with languages. - [Location events](https://ez-systems-developer-documentation--3381.com.readthedocs.build/en/3381/api/event_reference/location_events/): Events that are triggered when working with content Locations. - [Object state events](https://ez-systems-developer-documentation--3381.com.readthedocs.build/en/3381/api/event_reference/object_state_events/): Events that are triggered when working with object states and object state groups. - [Other events](https://ez-systems-developer-documentation--3381.com.readthedocs.build/en/3381/api/event_reference/other_events/): Events that are triggered when working with bookmarks, notifications, settings, forms and others. - [Page events](https://ez-systems-developer-documentation--3381.com.readthedocs.build/en/3381/api/event_reference/page_events/): Events that are triggered when working with pages and page blocks. - [Role events](https://ez-systems-developer-documentation--3381.com.readthedocs.build/en/3381/api/event_reference/role_events/): Events that are triggered when working with roles. - [Section events](https://ez-systems-developer-documentation--3381.com.readthedocs.build/en/3381/api/event_reference/section_events/): Events that are triggered when working with sections. - [Segmentation events](https://ez-systems-developer-documentation--3381.com.readthedocs.build/en/3381/api/event_reference/segmentation_events/): Events that are triggered when working with segments. - [Site events](https://ez-systems-developer-documentation--3381.com.readthedocs.build/en/3381/api/event_reference/site_events/): Events that are triggered when working with sites. - [Taxonomy events](https://ez-systems-developer-documentation--3381.com.readthedocs.build/en/3381/api/event_reference/taxonomy_events/): Events that are triggered when working with taxonomy. - [Translations management events](https://ez-systems-developer-documentation--3381.com.readthedocs.build/en/3381/api/event_reference/translations_management_events/): Events that are triggered when working with translations management. - [Trash events](https://ez-systems-developer-documentation--3381.com.readthedocs.build/en/3381/api/event_reference/trash_events/): Events that are triggered when working with Trash. - [Twig Components events](https://ez-systems-developer-documentation--3381.com.readthedocs.build/en/3381/api/event_reference/twig_component_events/): Events that are triggered when rendering Twig Components. - [URL events](https://ez-systems-developer-documentation--3381.com.readthedocs.build/en/3381/api/event_reference/url_events/): Events that are triggered when working with URLs, URL aliases and URL wildcards. - [User events](https://ez-systems-developer-documentation--3381.com.readthedocs.build/en/3381/api/event_reference/user_events/): Events that are triggered when working with users and user groups. # Content events > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). Events that are triggered when working with content. | Event | Dispatched by | Properties | | ---------------------------------- | --------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------- | | `BeforeCreateContentDraftEvent` | `ContentService::createContentDraft` | `ContentInfo $contentInfo` `VersionInfo $versionInfo` `User $creator` `?Language $language` `?Content $contentDraft` | | `CreateContentDraftEvent` | `ContentService::createContentDraft` | `Content $contentDraft` `ContentInfo $contentInfo` `VersionInfo $versionInfo` `User $creator` `?Language $language` | | `BeforeCreateContentEvent` | `ContentService::createContent` | `ContentCreateStruct $contentCreateStruct` `array $locationCreateStructs` `?Content $content` `string[] or null $fieldIdentifiersToValidate` | | `CreateContentEvent` | `ContentService::createContent` | `ContentCreateStruct $contentCreateStruct` `array $locationCreateStructs` `Content $content` `string[] or null $fieldIdentifiersToValidate` | | `BeforeUpdateContentEvent` | `ContentService::updateContent` | `VersionInfo $versionInfo` `ContentUpdateStruct $contentUpdateStruct` `?Content $content` `string[] or null $fieldIdentifiersToValidate` | | `UpdateContentEvent` | `ContentService::updateContent` | `Content $content` `VersionInfo $versionInfo` `ContentUpdateStruct $contentUpdateStruct` `string[] or null $fieldIdentifiersToValidate` | | `BeforeUpdateContentMetadataEvent` | `ContentService::updateContentMetadata` | `ContentInfo $contentInfo` `ContentMetadataUpdateStruct $contentMetadataUpdateStruct` `?Content $content` | | `UpdateContentMetadataEvent` | `ContentService::updateContentMetadata` | `Content $content` `ContentInfo $contentInfo` `ContentMetadataUpdateStruct $contentMetadataUpdateStruct` | | `BeforeCopyContentEvent` | `ContentService::copyContent` | `ContentInfo $contentInfo` `LocationCreateStruct $destinationLocationCreateStruct` `VersionInfo $versionInfo` `?Content $content` | | `CopyContentEvent` | `ContentService::copyContent` | `Content $content` `ContentInfo $contentInfo` `LocationCreateStruct $destinationLocationCreateStruct` `VersionInfo $versionInfo` | | `BeforePublishVersionEvent` | `ContentService::publishVersion` | `VersionInfo $versionInfo` `?Content $content` `string[] $translations` | | `PublishVersionEvent` | `ContentService::publishVersion` | `Content $content` `VersionInfo $versionInfo` `string[] $translations` | | `BeforeDeleteContentEvent` | `ContentService::deleteContent` | `ContentInfo $contentInfo` `array or null $locations` | | `DeleteContentEvent` | `ContentService::deleteContent` | `array $locations` `ContentInfo $contentInfo` | | `BeforeDeleteVersionEvent` | `ContentService::deleteVersion` | `VersionInfo $versionInfo` | | `DeleteVersionEvent` | `ContentService::deleteVersion` | `VersionInfo $versionInfo` | ## Relations | Event | Dispatched by | Properties | | --------------------------- | -------------------------------- | ------------------------------------------------------------------------------------ | | `BeforeAddRelationEvent` | `ContentService::addRelation` | `VersionInfo $sourceVersion` `ContentInfo $destinationContent` `?Relation $relation` | | `AddRelationEvent` | `ContentService::addRelation` | `Relation $relation` `VersionInfo $sourceVersion` `ContentInfo $destinationContent` | | `BeforeDeleteRelationEvent` | `ContentService::deleteRelation` | `VersionInfo $sourceVersion` `ContentInfo $destinationContent` | | `DeleteRelationEvent` | `ContentService::deleteRelation` | `VersionInfo $sourceVersion` `ContentInfo $destinationContent` | ## Content translations | Event | Dispatched by | Properties | | ------------------------------ | ----------------------------------- | ------------------------------------------ | | `BeforeDeleteTranslationEvent` | `ContentService::deleteTranslation` | `ContentInfo $contentInfo` `$languageCode` | | `DeleteTranslationEvent` | `ContentService::deleteTranslation` | `ContentInfo $contentInfo` `$languageCode` | ## Hiding and revealing | Event | Dispatched by | Properties | | -------------------------- | ------------------------------- | -------------------------- | | `BeforeHideContentEvent` | `ContentService::hideContent` | `ContentInfo $contentInfo` | | `HideContentEvent` | `ContentService::hideContent` | `ContentInfo $contentInfo` | | `BeforeRevealContentEvent` | `ContentService::revealContent` | `ContentInfo $contentInfo` | | `RevealContentEvent` | `ContentService::revealContent` | `ContentInfo $contentInfo` | # Content type events > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). Events that are triggered when working with content types. | Event | Dispatched by | Properties | | ------------------------------------ | --------------------------------------------- | ------------------------------------------------------------------------------------------------------------------- | | `BeforeCreateContentTypeDraftEvent` | `ContentTypeService::createContentTypeDraft` | `ContentType $contentType` `?ContentTypeDraft $contentTypeDraft` | | `CreateContentTypeDraftEvent` | `ContentTypeService::createContentTypeDraft` | `ContentTypeDraft $contentTypeDraft` `ContentType $contentType` | | `BeforeCreateContentTypeEvent` | `ContentTypeService::createContentType` | `ContentTypeCreateStruct $contentTypeCreateStruct` `array $contentTypeGroups` `?ContentTypeDraft $contentTypeDraft` | | `CreateContentTypeEvent` | `ContentTypeService::createContentType` | `ContentTypeDraft $contentTypeDraft` `ContentTypeCreateStruct $contentTypeCreateStruct` `array $contentTypeGroups` | | `BeforeUpdateContentTypeDraftEvent` | `ContentTypeService::updateContentTypeDraft` | `ContentTypeDraft $contentTypeDraft` `ContentTypeUpdateStruct $contentTypeUpdateStruct` | | `UpdateContentTypeDraftEvent` | `ContentTypeService::updateContentTypeDraft` | `ContentTypeDraft $contentTypeDraft` `ContentTypeUpdateStruct $contentTypeUpdateStruct` | | `BeforeCopyContentTypeEvent` | `ContentTypeService::copyContentType` | `ContentType $contentType` `User $creator` `?ContentType $contentTypeCopy` | | `CopyContentTypeEvent` | `ContentTypeService::copyContentType` | `ContentType $contentTypeCopy` `ContentType $contentType` `User $creator` | | `BeforePublishContentTypeDraftEvent` | `ContentTypeService::publishContentTypeDraft` | `ContentTypeDraft $contentTypeDraft` | | `PublishContentTypeDraftEvent` | `ContentTypeService::publishContentTypeDraft` | `ContentTypeDraft $contentTypeDraft` | | `BeforeDeleteContentTypeEvent` | `ContentTypeService::deleteContentType` | `ContentType $contentType` | | `DeleteContentTypeEvent` | `ContentTypeService::deleteContentType` | `ContentType $contentType` | ## Content type groups | Event | Dispatched by | Properties | | ----------------------------------- | -------------------------------------------- | ------------------------------------------------------------------------------------------------------------------- | | `BeforeCreateContentTypeGroupEvent` | `ContentTypeService::createContentTypeGroup` | `ContentTypeCreateStruct $contentTypeCreateStruct` `array $contentTypeGroups` `?ContentTypeDraft $contentTypeDraft` | | `CreateContentTypeGroupEvent` | `ContentTypeService::createContentTypeGroup` | `ContentTypeGroup $contentTypeGroup` `ContentTypeGroupCreateStruct $contentTypeGroupCreateStruct` | | `BeforeUpdateContentTypeGroupEvent` | `ContentTypeService::updateContentTypeGroup` | `ContentTypeGroup $contentTypeGroup` `ContentTypeGroupUpdateStruct $contentTypeGroupUpdateStruct` | | `UpdateContentTypeGroupEvent` | `ContentTypeService::updateContentTypeGroup` | `ContentTypeGroup $contentTypeGroup` `ContentTypeGroupUpdateStruct $contentTypeGroupUpdateStruct` | | `BeforeDeleteContentTypeGroupEvent` | `ContentTypeService::deleteContentTypeGroup` | `ContentTypeGroup $contentTypeGroup` | | `DeleteContentTypeGroupEvent` | `ContentTypeService::deleteContentTypeGroup` | `ContentTypeGroup $contentTypeGroup` | ## Content type translations | Event | Dispatched by | Properties | | ----------------------------------------- | -------------------------------------------------- | ---------------------------------------------------------------------------------------------------- | | `BeforeRemoveContentTypeTranslationEvent` | `ContentTypeService::removeContentTypeTranslation` | `ContentTypeDraft $contentTypeDraft` `string $languageCode` `?ContentTypeDraft $newContentTypeDraft` | | `RemoveContentTypeTranslationEvent` | `ContentTypeService::removeContentTypeTranslation` | `ContentTypeDraft $newContentTypeDraft` `ContentTypeDraft $contentTypeDraft` `string $languageCode` | ## Field definitions | Event | Dispatched by | Properties | | ---------------------------------- | ------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------- | | `BeforeAddFieldDefinitionEvent` | `ContentTypeService::addFieldDefinition` | `ContentTypeDraft $contentTypeDraft` `FieldDefinitionCreateStruct $fieldDefinitionCreateStruct` | | `AddFieldDefinitionEvent` | `ContentTypeService::addFieldDefinition` | `ContentTypeDraft $contentTypeDraft` `FieldDefinitionCreateStruct $fieldDefinitionCreateStruct` | | `BeforeUpdateFieldDefinitionEvent` | `ContentTypeService::updateFieldDefinition` | `ContentTypeDraft $contentTypeDraft` `FieldDefinition $fieldDefinition` `FieldDefinitionUpdateStruct $fieldDefinitionUpdateStruct` | | `UpdateFieldDefinitionEvent` | `ContentTypeService::updateFieldDefinition` | `ContentTypeDraft $contentTypeDraft` `FieldDefinition $fieldDefinition` `FieldDefinitionUpdateStruct $fieldDefinitionUpdateStruct` | | `BeforeRemoveFieldDefinitionEvent` | `ContentTypeService::removeFieldDefinition` | `ContentTypeDraft $contentTypeDraft` `FieldDefinition $fieldDefinition` | | `RemoveFieldDefinitionEvent` | `ContentTypeService::removeFieldDefinition` | `ContentTypeDraft $contentTypeDraft` `FieldDefinition $fieldDefinition` | ## Assigning to groups | Event | Dispatched by | Properties | | ------------------------------------- | ---------------------------------------------- | --------------------------------------------------------------- | | `BeforeAssignContentTypeGroupEvent` | `ContentTypeService::assignContentTypeGroup` | `ContentType $contentType` `ContentTypeGroup $contentTypeGroup` | | `AssignContentTypeGroupEvent` | `ContentTypeService::assignContentTypeGroup` | `ContentType $contentType` `ContentTypeGroup $contentTypeGroup` | | `BeforeUnassignContentTypeGroupEvent` | `ContentTypeService::unassignContentTypeGroup` | `ContentType $contentType` `ContentTypeGroup $contentTypeGroup` | | `UnassignContentTypeGroupEvent` | `ContentTypeService::unassignContentTypeGroup` | `ContentType $contentType` `ContentTypeGroup $contentTypeGroup` | # Location events > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). Events that are triggered when working with content Locations. | Event | Dispatched by | Properties | | --------------------------- | --------------------------------- | ---------------------------------------------------------------------------------------------- | | `BeforeCreateLocationEvent` | `LocationService::createLocation` | `ContentInfo $contentInfo` `LocationCreateStruct $locationCreateStruct` `?Location $location` | | `CreateLocationEvent` | `LocationService::createLocation` | `Location $location` `ContentInfo $contentInfo` `LocationCreateStruct $locationCreateStruct` | | `BeforeUpdateLocationEvent` | `LocationService::updateLocation` | `Location $location` `LocationUpdateStruct $locationUpdateStruct` `?Location $updatedLocation` | | `UpdateLocationEvent` | `LocationService::updateLocation` | `Location $updatedLocation` `Location $location` `LocationUpdateStruct $locationUpdateStruct` | | `BeforeDeleteLocationEvent` | `LocationService::deleteLocation` | `Location $location` | | `DeleteLocationEvent` | `LocationService::deleteLocation` | `Location $location` | ## Hiding and revealing | Event | Dispatched by | Properties | | --------------------------- | --------------------------------- | -------------------------------------------------- | | `BeforeHideLocationEvent` | `LocationService::hideLocation` | `Location $location` `?Location $hiddenLocation` | | `HideLocationEvent` | `LocationService::hideLocation` | `Location $hiddenLocation` `Location $location` | | `BeforeUnhideLocationEvent` | `LocationService::unhideLocation` | `Location $location` `?Location $revealedLocation` | | `UnhideLocationEvent` | `LocationService::unhideLocation` | `Location $revealedLocation` `Location $location` | ## Subtree and Location management | Event | Dispatched by | Properties | | ------------------------- | ------------------------------- | -------------------------------------------------------------------------- | | `BeforeCopySubtreeEvent` | `LocationService::copySubtree` | `Location $subtree` `Location $targetParentLocation` `?Location $location` | | `CopySubtreeEvent` | `LocationService::copySubtree` | `Location $location` `Location $subtree` `Location $targetParentLocation` | | `BeforeMoveSubtreeEvent` | `LocationService::moveSubtree` | `Location $location` `Location $newParentLocation` | | `MoveSubtreeEvent` | `LocationService::moveSubtree` | `Location $location` `Location $newParentLocation` | | `BeforeSwapLocationEvent` | `LocationService::swapLocation` | `Location $location1` `Location $location2` | | `SwapLocationEvent` | `LocationService::swapLocation` | `Location $location1` `Location $location2` | # Language events > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). Events that are triggered when working with languages. | Event | Dispatched by | Properties | | ------------------------------- | ------------------------------------- | ------------------------------------------------------------------- | | `BeforeCreateLanguageEvent` | `LanguageService::createLanguage` | `LanguageCreateStruct $languageCreateStruct` `?Language $language` | | `CreateLanguageEvent` | `LanguageService::createLanguage` | `Language $language` `LanguageCreateStruct $languageCreateStruct` | | `BeforeUpdateLanguageNameEvent` | `LanguageService::updateLanguageName` | `Language $language` `string $newName` `?Language $updatedLanguage` | | `UpdateLanguageNameEvent` | `LanguageService::updateLanguageName` | `Language $updatedLanguage` `Language $language` `string $newName` | | `BeforeDeleteLanguageEvent` | `LanguageService::deleteLanguage` | `Language $language` | | `DeleteLanguageEvent` | `LanguageService::deleteLanguage` | `Language $language` | ## Enabling languages | Event | Dispatched by | Properties | | ---------------------------- | ---------------------------------- | -------------------------------------------------- | | `BeforeEnableLanguageEvent` | `LanguageService::enableLanguage` | `Language $language` `?Language $enabledLanguage` | | `EnableLanguageEvent` | `LanguageService::enableLanguage` | `Language $enabledLanguage` `Language $language` | | `BeforeDisableLanguageEvent` | `LanguageService::disableLanguage` | `Language $language` `?Language $disabledLanguage` | | `DisableLanguageEvent` | `LanguageService::disableLanguage` | `Language $disabledLanguage` `Language $language` | # Section events > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). Events that are triggered when working with sections. | Event | Dispatched by | Properties | | -------------------------- | ------------------------------- | ---------------------------------------------------------------------------------------- | | `BeforeCreateSectionEvent` | `SectionService::createSection` | `SectionCreateStruct $sectionCreateStruct` `?Section $section` | | `CreateSectionEvent` | `SectionService::createSection` | `SectionCreateStruct $sectionCreateStruct` `Section $section` | | `BeforeDeleteSectionEvent` | `SectionService::deleteSection` | `Section $section` | | `DeleteSectionEvent` | `SectionService::deleteSection` | `Section $section` | | `BeforeUpdateSectionEvent` | `SectionService::updateSection` | `Section $section` `SectionUpdateStruct $sectionUpdateStruct` `?Section $updatedSection` | | `UpdateSectionEvent` | `SectionService::updateSection` | `Section $section` `SectionUpdateStruct $sectionUpdateStruct` `Section $updatedSection` | ## Assigning sections | Event | Dispatched by | Properties | | ----------------------------------- | ---------------------------------------- | --------------------------------------------- | | `BeforeAssignSectionEvent` | `SectionService::assignSection` | `ContentInfo $contentInfo` `Section $section` | | `AssignSectionEvent` | `SectionService::assignSection` | `ContentInfo $contentInfo` `Section $section` | | `BeforeAssignSectionToSubtreeEvent` | `SectionService::assignSectionToSubtree` | `Location $location` `Section $section` | | `AssignSectionToSubtreeEvent` | `SectionService::assignSectionToSubtree` | `Location $location` `Section $section` | # Object state events > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). Events that are triggered when working with object states and object state groups. | Event | Dispatched by | Properties | | ------------------------------ | --------------------------------------- | ------------------------------------------------------------------------------------------------------------------- | | `BeforeCreateObjectStateEvent` | `ObjectStateService::createObjectState` | `ObjectStateGroup $objectStateGroup` `ObjectStateCreateStruct $objectStateCreateStruct` `?ObjectState $objectState` | | `CreateObjectStateEvent` | `ObjectStateService::createObjectState` | `ObjectState $objectState` `ObjectStateGroup $objectStateGroup` `ObjectStateCreateStruct $objectStateCreateStruct` | | `BeforeUpdateObjectStateEvent` | `ObjectStateService::updateObjectState` | `ObjectState $objectState` `ObjectStateUpdateStruct $objectStateUpdateStruct` `?ObjectState $updatedObjectState` | | `UpdateObjectStateEvent` | `ObjectStateService::updateObjectState` | `ObjectState $updatedObjectState` `ObjectState $objectState` `ObjectStateUpdateStruct $objectStateUpdateStruct` | | `BeforeDeleteObjectStateEvent` | `ObjectStateService::deleteObjectState` | `ObjectState $objectState` | | `DeleteObjectStateEvent` | `ObjectStateService::deleteObjectState` | `ObjectState $objectState` | ## Object state groups | Event | Dispatched by | Properties | | ----------------------------------- | -------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- | | `BeforeCreateObjectStateGroupEvent` | `ObjectStateService::createObjectStateGroup` | `ObjectStateGroupCreateStruct $objectStateGroupCreateStruct` `?ObjectStateGroup $objectStateGroup` | | `CreateObjectStateGroupEvent` | `ObjectStateService::createObjectStateGroup` | `ObjectStateGroup $objectStateGroup` `ObjectStateGroupCreateStruct $objectStateGroupCreateStruct` | | `BeforeUpdateObjectStateGroupEvent` | `ObjectStateService::updateObjectStateGroup` | `ObjectStateGroup $objectStateGroup` `ObjectStateGroupUpdateStruct $objectStateGroupUpdateStruct` `?ObjectStateGroup $updatedObjectStateGroup` | | `UpdateObjectStateGroupEvent` | `ObjectStateService::updateObjectStateGroup` | `ObjectStateGroup $updatedObjectStateGroup` `ObjectStateGroup $objectStateGroup` `ObjectStateGroupUpdateStruct $objectStateGroupUpdateStruct` | | `BeforeDeleteObjectStateGroupEvent` | `ObjectStateService::deleteObjectStateGroup` | `ObjectStateGroup $objectStateGroup` | | `DeleteObjectStateGroupEvent` | `ObjectStateService::deleteObjectStateGroup` | `ObjectStateGroup $objectStateGroup` | ## Setting states | Event | Dispatched by | Properties | | ------------------------------------- | ---------------------------------------------- | ------------------------------------------------------------------------------------------ | | `BeforeSetContentStateEvent` | `ObjectStateService::deleteObjectState` | `ContentInfo $contentInfo` `ObjectStateGroup $objectStateGroup` `ObjectState $objectState` | | `SetContentStateEvent` | `ObjectStateService::deleteObjectState` | `ContentInfo $contentInfo` `ObjectStateGroup $objectStateGroup` `ObjectState $objectState` | | `BeforeSetPriorityOfObjectStateEvent` | `ObjectStateService::setPriorityOfObjectState` | `ObjectState $objectState` `private $priority` | | `SetPriorityOfObjectStateEvent` | `ObjectStateService::setPriorityOfObjectState` | `ObjectState $objectState` `private $priority` | # Taxonomy events > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). Events that are triggered when working with taxonomy. The following Events are dispatched when managing [taxonomy entries](https://doc.ibexa.co/en/saas/content_management/taxonomy/taxonomy/index.md). | Event | Dispatched by | Properties | | ----------------------------------------------- | --------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------- | | `BeforeCreateTaxonomyEntryEvent` | `TaxonomyService::createEntry` | `TaxonomyEntryCreateStruct $createStruct` `?TaxonomyEntry $taxonomyEntry = null` | | `CreateTaxonomyEntryEvent` | `TaxonomyService::createEntry` | `TaxonomyEntry $taxonomyEntry` `TaxonomyEntryCreateStruct $createStruct` | | `BeforeMoveTaxonomyEntryEvent` | `TaxonomyService::moveEntry` | `TaxonomyEntry $taxonomyEntry` `TaxonomyEntry $newParent` | | `MoveTaxonomyEntryEvent` | `TaxonomyService::moveEntry` | `TaxonomyEntry $taxonomyEntry` `TaxonomyEntry $newParent` | | `BeforeMoveTaxonomyEntryRelativeToSiblingEvent` | `TaxonomyService::moveEntryRelativeToSibling` | `TaxonomyEntry $taxonomyEntry` `TaxonomyEntry $sibling` `string $position` | | `MoveTaxonomyEntryRelativeToSiblingEvent` | `TaxonomyService::moveEntryRelativeToSibling` | `TaxonomyEntry $taxonomyEntry` `TaxonomyEntry $sibling` `string $position` | | `BeforeRemoveTaxonomyEntryEvent` | `TaxonomyService::removeEntry` | `TaxonomyEntry $taxonomyEntry` | | `RemoveTaxonomyEntryEvent` | `TaxonomyService::removeEntry` | `TaxonomyEntry $taxonomyEntry` | | `BeforeUpdateTaxonomyEntryEvent` | `TaxonomyService::updateEntry` | `TaxonomyEntry $taxonomyEntry` `TaxonomyEntryUpdateStruct $updateStruct` `?TaxonomyEntry $updatedTaxonomyEntry = null` | | `UpdateTaxonomyEntryEvent` | `TaxonomyService::updateEntry` | `TaxonomyEntry $updatedTaxonomyEntry` `TaxonomyEntry $taxonomyEntry` `TaxonomyEntryUpdateStruct $updateStruct` | # Role events > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). Events that are triggered when working with roles. | Event | Dispatched by | Properties | | ----------------------------- | ------------------------------- | ------------------------------------------------------------------------------------------ | | `BeforeCreateRoleDraftEvent` | `RoleService::createRoleDraft` | `Role $role` `?RoleDraft $roleDraft` | | `CreateRoleDraftEvent` | `RoleService::createRoleDraft` | `Role $role` `RoleDraft $roleDraft` | | `BeforeCreateRoleEvent` | `RoleService::createRole` | `RoleCreateStruct $roleCreateStruct` `?RoleDraft $roleDraft` | | `CreateRoleEvent` | `RoleService::createRole` | `RoleCreateStruct $roleCreateStruct` `RoleDraft $roleDraft` | | `BeforeUpdateRoleDraftEvent` | `RoleService::updateRoleDraft` | `RoleDraft $roleDraft` `RoleUpdateStruct $roleUpdateStruct` `?RoleDraft $updatedRoleDraft` | | `UpdateRoleDraftEvent` | `RoleService::updateRoleDraft` | `RoleDraft $roleDraft` `RoleUpdateStruct $roleUpdateStruct` `RoleDraft $updatedRoleDraft` | | `BeforeCopyRoleEvent` | `RoleService::copyRole` | `Role $role` `RoleCopyStruct $roleCopyStruct` `?Role $copiedRole` | | `CopyRoleEvent` | `RoleService::copyRole` | `Role $copiedRole` `Role $role` `RoleCopyStruct $roleCopyStruct` | | `BeforePublishRoleDraftEvent` | `RoleService::publishRoleDraft` | `RoleDraft $roleDraft` | | `PublishRoleDraftEvent` | `RoleService::publishRoleDraft` | `RoleDraft $roleDraft` | | `BeforeDeleteRoleDraftEvent` | `RoleService::deleteRoleDraft` | `RoleDraft $roleDraft` | | `DeleteRoleDraftEvent` | `RoleService::deleteRoleDraft` | `RoleDraft $roleDraft` | | `BeforeDeleteRoleEvent` | `RoleService::deleteRole` | `Role $role` | | `DeleteRoleEvent` | `RoleService::deleteRole` | `Role $role` | ## Adding policies | Event | Dispatched by | Properties | | ------------------------------------ | -------------------------------------- | ------------------------------------------------------------------------------------------------------------------------ | | `BeforeAddPolicyByRoleDraftEvent` | `RoleService::addPolicyByRoleDraft` | `RoleDraft $roleDraft` `PolicyCreateStruct $policyCreateStruct` `?RoleDraft $updatedRoleDraft` | | `AddPolicyByRoleDraftEvent` | `RoleService::addPolicyByRoleDraft` | `RoleDraft $roleDraft` `PolicyCreateStruct $policyCreateStruct` `private $updatedRoleDraft` | | `BeforeUpdatePolicyByRoleDraftEvent` | `RoleService::updatePolicyByRoleDraft` | `RoleDraft $roleDraft` `PolicyDraft $policy` `PolicyUpdateStruct $policyUpdateStruct` `?PolicyDraft $updatedPolicyDraft` | | `UpdatePolicyByRoleDraftEvent` | `RoleService::updatePolicyByRoleDraft` | `RoleDraft $roleDraft` `PolicyDraft $policy` `PolicyUpdateStruct $policyUpdateStruct` `PolicyDraft $updatedPolicyDraft` | | `BeforeRemovePolicyByRoleDraftEvent` | `RoleService::removePolicyByRoleDraft` | `RoleDraft $roleDraft` `PolicyDraft $policyDraft` `?RoleDraft $updatedRoleDraft` | | `RemovePolicyByRoleDraftEvent` | `RoleService::removePolicyByRoleDraft` | `RoleDraft $roleDraft` `PolicyDraft $policyDraft` `RoleDraft $updatedRoleDraft` | ## Assigning roles | Event | Dispatched by | Properties | | ---------------------------------- | ------------------------------------ | ------------------------------------------------------------------------------- | | `BeforeAssignRoleToUserEvent` | `RoleService::assignRoleToUser` | `Role $role` `User $user` `Limitation\RoleLimitation $roleLimitation` | | `AssignRoleToUserEvent` | `RoleService::assignRoleToUser` | `Role $role` `User $user` `Limitation\RoleLimitation $roleLimitation` | | `BeforeAssignRoleToUserGroupEvent` | `RoleService::assignRoleToUserGroup` | `Role $role` `UserGroup $userGroup` `Limitation\RoleLimitation $roleLimitation` | | `AssignRoleToUserGroupEvent` | `RoleService::assignRoleToUserGroup` | `Role $role` `UserGroup $userGroup` `Limitation\RoleLimitation $roleLimitation` | | `BeforeRemoveRoleAssignmentEvent` | `RoleService::removeRoleAssignment` | `RoleAssignment $roleAssignment` | | `RemoveRoleAssignmentEvent` | `RoleService::removeRoleAssignment` | `RoleAssignment $roleAssignment` | # User events > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). Events that are triggered when working with users and user groups. | Event | Dispatched by | Properties | | ----------------------- | ------------------------- | ------------------------------------------------------------------------ | | `BeforeCreateUserEvent` | `UserService::createUser` | `UserCreateStruct $userCreateStruct` `array $parentGroups` `?User $user` | | `CreateUserEvent` | `UserService::createUser` | `UserCreateStruct $userCreateStruct` `array $parentGroups` `User $user` | | `BeforeUpdateUserEvent` | `UserService::updateUser` | `User $user` `UserUpdateStruct $userUpdateStruct` `?User $updatedUser` | | `UpdateUserEvent` | `UserService::updateUser` | `User $user` `UserUpdateStruct $userUpdateStruct` `User $updatedUser` | | `BeforeDeleteUserEvent` | `UserService::deleteUser` | `User $user` `array or null $locations` | | `DeleteUserEvent` | `UserService::deleteUser` | `User $user` `array $locations` | ## User groups | Event | Dispatched by | Properties | | ---------------------------- | ------------------------------ | ---------------------------------------------------------------------------------------------------- | | `BeforeCreateUserGroupEvent` | `UserService::createUserGroup` | `UserGroupCreateStruct $userGroupCreateStruct` `UserGroup $parentGroup` `?UserGroup $userGroup` | | `CreateUserGroupEvent` | `UserService::createUserGroup` | `UserGroupCreateStruct $userGroupCreateStruct` `UserGroup $parentGroup` `UserGroup $userGroup` | | `BeforeUpdateUserGroupEvent` | `UserService::updateUserGroup` | `UserGroup $userGroup` `UserGroupUpdateStruct $userGroupUpdateStruct` `?UserGroup $updatedUserGroup` | | `UpdateUserGroupEvent` | `UserService::updateUserGroup` | `UserGroup $userGroup` `UserGroupUpdateStruct $userGroupUpdateStruct` | | `BeforeDeleteUserGroupEvent` | `UserService::deleteUserGroup` | `UserGroup $userGroup` `array or null $locations` | | `DeleteUserGroupEvent` | `UserService::deleteUserGroup` | `UserGroup $userGroup` `array $locations` | | `BeforeMoveUserGroupEvent` | `UserService::moveUserGroup` | `UserGroup $userGroup` `UserGroup $newParent` | | `MoveUserGroupEvent` | `UserService::moveUserGroup` | `UserGroup $userGroup` `UserGroup $newParent` | ## Assigning to user groups | Event | Dispatched by | Properties | | -------------------------------------- | ---------------------------------------- | ----------------------------------- | | `BeforeAssignUserToUserGroupEvent` | `UserService::assignUserToUserGroup` | `User $user` `UserGroup $userGroup` | | `AssignUserToUserGroupEvent` | `UserService::assignUserToUserGroup` | `User $user` `UserGroup $userGroup` | | `BeforeUnAssignUserFromUserGroupEvent` | `UserService::unAssignUserFromUserGroup` | `User $user` `UserGroup $userGroup` | | `UnAssignUserFromUserGroupEvent` | `UserService::unAssignUserFromUserGroup` | `User $user` `UserGroup $userGroup` | ## Updating User data | Event | Dispatched by | Properties | | ------------------------------- | --------------------------------- | -------------------------------------------------------------------------------- | | `BeforeUpdateUserPasswordEvent` | `UserService::updateUserPassword` | `User $user` `string $newPassword` `?User $updatedUser` | | `UpdateUserPasswordEvent` | `UserService::updateUserPassword` | `User $user` `string $newPassword` `User $updatedUser` | | `BeforeUpdateUserTokenEvent` | `UserService::updateUserToken` | `User $user` `UserTokenUpdateStruct $userTokenUpdateStruct` `?User $updatedUser` | | `UpdateUserTokenEvent` | `UserService::updateUserToken` | `User $user` `UserTokenUpdateStruct $userTokenUpdateStruct` `User $updatedUser` | # Segmentation events > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). Events that are triggered when working with segments. Editions: Experience | Event | Dispatched by | Properties | | ------------------------------- | ----------------------------------------- | ---------------------------------------------------------------------------------------------------------------- | | `BeforeCreateSegmentGroupEvent` | `SegmentationService::createSegmentGroup` | `SegmentGroupCreateStruct $createStruct` `?SegmentGroup $segmentGroupResult = null` | | `CreateSegmentGroupEvent` | `SegmentationService::createSegmentGroup` | `SegmentGroupCreateStruct $createStruct` `SegmentGroup $segmentGroupResult` | | `BeforeUpdateSegmentGroupEvent` | `SegmentationService::updateSegmentGroup` | `SegmentGroup $segmentGroup` `SegmentGroupUpdateStruct $updateStruct` `?SegmentGroup $segmentGroupResult = null` | | `UpdateSegmentGroupEvent` | `SegmentationService::updateSegmentGroup` | `SegmentGroup $segmentGroup` `SegmentGroupUpdateStruct $updateStruct` `SegmentGroup $segmentGroupResult` | | `BeforeRemoveSegmentGroupEvent` | `SegmentationService::removeSegmentGroup` | `SegmentGroup $segmentGroup` | | `RemoveSegmentGroupEvent` | `SegmentationService::removeSegmentGroup` | `SegmentGroup $segmentGroup` | | `BeforeCreateSegmentEvent` | `SegmentationService::createSegment` | `SegmentCreateStruct $createStruct` `?Segment $segmentResult = null` | | `CreateSegmentEvent` | `SegmentationService::createSegment` | `SegmentCreateStruct $createStruct` `Segment $segmentResult` | | `BeforeUpdateSegmentEvent` | `SegmentationService::updateSegment` | `Segment $segment` `SegmentUpdateStruct $updateStruct` `?Segment $segmentResult = null` | | `UpdateSegmentEvent` | `SegmentationService::updateSegment` | `Segment $segment` `SegmentUpdateStruct $updateStruct` `Segment $segmentResult` | | `BeforeRemoveSegmentEvent` | `SegmentationService::removeSegment` | `Segment $segment` | | `RemoveSegmentEvent` | `SegmentationService::removeSegment` | `Segment $segment` | ## Assigning segments | Event | Dispatched by | Properties | | ---------------------------------------- | ---------------------------------------------- | ------------------------------- | | `BeforeAssignUserToSegmentEvent.php` | `SegmentationService::assignUserToSegment` | `User $user` `Segment $segment` | | `AssignUserToSegmentEvent.php` | `SegmentationService::assignUserToSegment` | `User $user` `Segment $segment` | | `BeforeUnassignUserFromSegmentEvent.php` | `SegmentationService::unassignUserFromSegment` | `User $user` `Segment $segment` | | `UnassignUserFromSegmentEvent.php` | `SegmentationService::unassignUserFromSegment` | `User $user` `Segment $segment` | # Page events > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). Events that are triggered when working with pages and page blocks. Editions: Experience | Event | Dispatched by | Properties | | ----------------------------- | ----------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `AttributeSerializationEvent` | `AttributeSerializationDispatcher::serialize` | `LandingPage\Model\BlockValue $blockValue` `string $attributeIdentifier` `mixed $serializedValue` `mixed $deserializedValue` | | `BlockContextEvent` | `BlockService::createBlockContextFromRequest` | `Request $request` `?BlockContextInterface $blockContext` | | `BlockFragmentRenderEvent` | `BlockRenderOptionsFragmentRenderer::dispatchFragmentRenderEvent` | `Content $content` `?Location $location` `LandingPage\Model\Page $page` `LandingPage\Model\BlockValue $blockValue` `ControllerReference $uri` `Request $request` `array $options` | | `BlockResponseEvent` | `BlockResponseSubscriber::getSubscribedEvents` | `BlockContextInterface $blockContext` `LandingPage\Model\BlockValue $blockValue` `Request $request` `Response $response` | | `CollectBlockRelationsEvent` | `CollectRelationsSubscriber::onCollectBlockRelations` | `LandingPage\Value $fieldValue` `LandingPage\Model\BlockValue $blockValue` `int[] $relations` | | `PageRenderEvent` | `PageService::dispatchRenderPageEvent` | `Content $content` `?Location $location` `LandingPage\Model\Page $page` `Request $request` | ## Page Builder The following events are dispatched when editing a page in the Page Builder. | Event | Dispatched by | Properties | | ------------------------------ | ------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `BlockConfigurationViewEvent` | `BlockController::dispatchBlockConfigurationViewEvent` | `BlockConfigurationView $blockConfigurationView` `BlockDefinition $blockDefinition` `BlockConfiguration $blockConfiguration` `FormInterface $blockConfigurationForm` | | `BlockPreviewPageContextEvent` | `PreviewController::dispatchPageContextEvent` | `BlockContextInterface $blockContext` `LandingPage\Model\Page $page` `array $pagePreviewParameters` | | `BlockPreviewResponseEvent` | `PreviewController::dispatchBlockPreviewResponseEvent` | `BlockContextInterface $blockContext` `array $pagePreviewParameters` `LandingPage\Model\Page $page` `BlockValue $blockValue` `array $responseData` | ## Page blocks The following events are dispatched when editing a page block. | Event | Dispatched by | Properties | | ------------------------------- | -------------------------------------------- | ------------------------------------------------------------------------------------------------------ | | `BlockDefinitionEvent` | `BlockDefinitionFactory::getBlockDefinition` | `BlockDefinition $definition` `array $configuration` | | `BlockAttributeDefinitionEvent` | `BlockDefinitionFactory::getBlockDefinition` | `BlockAttributeDefinition $definition` `array $configuration` | | `PreRenderEvent` | `BlockService::render` | `BlockContextInterface $blockContext` `BlockValue $blockValue` `RenderRequestInterface $renderRequest` | | `PostRenderEvent` | `BlockService::render` | `BlockContextInterface $blockContext` `BlockValue $blockValue` `string $renderedBlock` | # Site events > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). Events that are triggered when working with sites. Editions: Experience The following events are dispatched when managing [Sites](https://doc.ibexa.co/en/saas/multisite/site_factory/site_factory/index.md). | Event | Dispatched by | Properties | | ----------------------- | ------------------------- | ---------------------------------------------------------------------- | | `BeforeCreateSiteEvent` | `SiteService::createSite` | `SiteCreateStruct $siteCreateStruct` `Site $site` | | `CreateSiteEvent` | `SiteService::createSite` | `Site $site` `SiteCreateStruct $siteCreateStruct` | | `BeforeUpdateSiteEvent` | `SiteService::updateSite` | `Site $site` `SiteUpdateStruct $siteUpdateStruct` `?Site $updatedSite` | | `UpdateSiteEvent` | `SiteService::updateSite` | `Site $updatedSite` `Site $site` `SiteUpdateStruct $siteUpdateStruct` | | `BeforeDeleteSiteEvent` | `SiteService::deleteSite` | `Site $site` | | `DeleteSiteEvent` | `SiteService::deleteSite` | `Site $site` | # URL events > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). Events that are triggered when working with URLs, URL aliases and URL wildcards. ## URLs | Event | Dispatched by | Properties | | ---------------------- | ----------------------- | ------------------------------------------------------- | | `BeforeUpdateUrlEvent` | `URLService::updateUrl` | `URL $url` `URLUpdateStruct $struct` `?URL $updatedUrl` | | `UpdateUrlEvent` | `URLService::updateUrl` | `URL $url` `URLUpdateStruct $struct` `URL $updatedUrl` | ## URL aliases The following events are dispatched when creating and managing [URL aliases](https://doc.ibexa.co/en/saas/content_management/url_management/url_management/#url-aliases). | Event | Dispatched by | Properties | | ----------------------------------------------- | ----------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------- | | `BeforeCreateGlobalUrlAliasEvent` | `URLAliasService::createGlobalUrlAlias` | `private $resource` `private $path` `private $languageCode` `private $forwarding` `private $alwaysAvailable` `?URLAlias $urlAlias` | | `CreateGlobalUrlAliasEvent` | `URLAliasService::createGlobalUrlAlias` | `private $resource` `private $path` `private $languageCode` `private $forwarding` `private $alwaysAvailable` `URLAlias $urlAlias` | | `BeforeCreateUrlAliasEvent` | `URLAliasService::createUrlAlias` | `Location $location` `private $path` `private $languageCode` `private $forwarding` `private $alwaysAvailable` `?URLAlias $urlAlias` | | `CreateUrlAliasEvent` | `URLAliasService::createUrlAlias` | `Location $location` `private $path` `private $languageCode` `private $forwarding` `private $alwaysAvailable` `URLAlias $urlAlias` | | `BeforeRefreshSystemUrlAliasesForLocationEvent` | `URLAliasService::refreshSystemUrlAliasesForLocation` | `Location $location` | | `RefreshSystemUrlAliasesForLocationEvent` | `URLAliasService::refreshSystemUrlAliasesForLocation` | `Location $location` | | `BeforeRemoveAliasesEvent` | `URLAliasService::removeAliases` | `array $aliasList` | | `RemoveAliasesEvent` | `URLAliasService::removeAliases` | `array $aliasList` | ## URL wildcards The following events are dispatched when creating and managing [URL wildcards](https://doc.ibexa.co/en/saas/content_management/url_management/url_management/#url-wildcards). | Event | Dispatched by | Properties | | ---------------------- | ------------------------------- | --------------------------------------------------------------------------------------------- | | `BeforeCreateEvent` | `URLWildcardService::create` | `private $sourceUrl` `private $destinationUrl` `private $forward` `?URLWildcard $urlWildcard` | | `CreateEvent` | `URLWildcardService::create` | `private $sourceUrl` `private $destinationUrl` `private $forward` `URLWildcard $urlWildcard` | | `BeforeUpdateEvent` | `URLWildcardService::update` | `URLWildcard $urlWildcard` `URLWildcardUpdateStruct $updateStruct` | | `UpdateEvent` | `URLWildcardService::update` | `URLWildcard $urlWildcard` `URLWildcardUpdateStruct $updateStruct` | | `BeforeTranslateEvent` | `URLWildcardService::translate` | `private $url` `?URLWildcardTranslationResult $result` | | `TranslateEvent` | `URLWildcardService::translate` | `private $url` `URLWildcardTranslationResult $result` | | `BeforeRemoveEvent` | `URLWildcardService::remove` | `URLWildcard $urlWildcard` | | `RemoveEvent` | `URLWildcardService::remove` | `URLWildcard $urlWildcard` | # Trash events > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). Events that are triggered when working with Trash. The following events are dispatched when managing Trash. | Event | Dispatched by | Properties | | ---------------------------- | ------------------------------- | -------------------------------------------------------------------------- | | `BeforeDeleteTrashItemEvent` | `TrashService::deleteTrashItem` | `TrashItem $trashItem` `?TrashItemDeleteResult $result` | | `DeleteTrashItemEvent` | `TrashService::deleteTrashItem` | `TrashItem $trashItem` `TrashItemDeleteResult $result` | | `BeforeEmptyTrashEvent` | `TrashService::emptyTrash` | `?TrashItemDeleteResultList $resultList` | | `EmptyTrashEvent` | `TrashService::emptyTrash` | `TrashItemDeleteResultList $resultList` | | `BeforeRecoverEvent` | `TrashService::recover` | `TrashItem $trashItem` `Location $newParentLocation` `?Location $location` | | `RecoverEvent` | `TrashService::recover` | `TrashItem $trashItem` `Location $newParentLocation` `Location $location` | | `BeforeTrashEvent` | `TrashService::trash` | `Location $location` `?TrashItem $result` `bool $resultSet = false` | | `TrashEvent` | `TrashService::trash` | `Location $location` `?TrashItem $trashItem` | # Twig Components events > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). Events that are triggered when rendering Twig Components. Use the events to hook into the rendering process of [Twig Components](https://doc.ibexa.co/en/saas/templating/components/index.md). ## Twig Component rendering | Event | Dispatched by | Description | | ------------------- | -------------------------------------------------------------------------- | ------------------------------------------------ | | `RenderGroupEvent` | `\Ibexa\TwigComponents\Component\Renderer\DefaultRenderer::renderGroup()` | Dispatched before a Component group is rendered | | `RenderSingleEvent` | `\Ibexa\TwigComponents\Component\Renderer\DefaultRenderer::renderSingle()` | Dispatched before a single Component is rendered | # AI Actions events > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). Events that are triggered when working with AI actions. ## AI Action execution | Event | Dispatched by | | --------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- | | [BeforeExecuteEvent](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-ConnectorAi-Action-Event-BeforeExecuteEvent.html) | [ActionServiceInterface](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-ConnectorAi-ActionServiceInterface.html) | | [ExecuteEvent](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-ConnectorAi-Action-Event-ExecuteEvent.html) | [ActionServiceInterface](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-ConnectorAi-ActionServiceInterface.html) | ## Action Configurations management | Event | Dispatched by | | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | [BeforeCreateActionConfigurationEvent](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-ConnectorAi-ActionConfiguration-Event-BeforeCreateActionConfigurationEvent.html) | [ActionConfigurationServiceInterface](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-ConnectorAi-ActionConfigurationServiceInterface.html) | | [CreateActionConfigurationEvent](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-ConnectorAi-ActionConfiguration-Event-CreateActionConfigurationEvent.html) | [ActionConfigurationServiceInterface](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-ConnectorAi-ActionConfigurationServiceInterface.html) | | [BeforeUpdateActionConfigurationEvent](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-ConnectorAi-ActionConfiguration-Event-BeforeUpdateActionConfigurationEvent.html) | [ActionConfigurationServiceInterface](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-ConnectorAi-ActionConfigurationServiceInterface.html) | | [UpdateActionConfigurationEvent](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-ConnectorAi-ActionConfiguration-Event-UpdateActionConfigurationEvent.html) | [ActionConfigurationServiceInterface](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-ConnectorAi-ActionConfigurationServiceInterface.html) | | [BeforeDeleteActionConfigurationEvent](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-ConnectorAi-ActionConfiguration-Event-BeforeDeleteActionConfigurationEvent.html) | [ActionConfigurationServiceInterface](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-ConnectorAi-ActionConfigurationServiceInterface.html) | | [DeleteActionConfigurationEvent](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-ConnectorAi-ActionConfiguration-Event-DeleteActionConfigurationEvent.html) | [ActionConfigurationServiceInterface](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-ConnectorAi-ActionConfigurationServiceInterface.html) | ## Others | Event | Dispatched by | Description | | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------- | | [ContextEvent](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-ConnectorAi-Events-ContextEvent.html) | [ActionServiceInterface](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-ConnectorAi-ActionServiceInterface.html) | Pass additional options to the System Context before an AI Action is executed | | [ResolveActionConfigurationWidgetConfigEvent](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-ConnectorAi-Events-ResolveActionConfigurationWidgetConfigEvent.html) | `\Ibexa\ConnectorAi\Twig\ActionConfigurationWidgetConfigExtension::renderActionConfigurationWidgetConfig()` | Modify the Action Type configuration returned from the `ibexa_ai_config` Twig function | | [ResolveActionHandlerEvent](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-ConnectorAi-Events-ResolveActionHandlerEvent.html) | [ActionHandlerResolverInterface](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-ConnectorAi-Action-ActionHandlerResolverInterface.html) | Hook into the process of choosing a Handler to execute an AI Action | # Integrated help events > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). Events that are triggered when working with integrated help features like product tours. Editions: LTS Update ## Product tour events The following event is dispatched when rendering a [product tour scenario](https://doc.ibexa.co/en/saas/administration/back_office/product_tour/index.md). | Event | Dispatched by | | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------- | | [`RenderProductTourScenarioEvent`](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-IntegratedHelp-Event-RenderProductTourScenarioEvent.html) | `Ibexa\IntegratedHelp\Renderer\ProductTourRenderer::render()` | To learn how you can use this event to customize your product tour scenarios, see [Customize product tour](https://doc.ibexa.co/en/saas/administration/back_office/customize_product_tour/index.md). # Translations management events > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). Events that are triggered when working with translations management. Editions: LTS Update The [Translations management](https://doc.ibexa.co/en/saas/multisite/translations_management/translations_management_guide/index.md) package dispatches events at two levels. ## Translation events Translation events are dispatched for every field in each selected target language. Use them for logging, analytics, and observability. Both events are read-only, you can't use them to override the translation result. | Event | Dispatched by | Dispatched when | | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------ | ---------------------------------------------------- | | [`BeforeTranslateEvent`](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-TranslationsManagement-AutoTranslate-Event-BeforeTranslateEvent.html) | `EventDispatchingProviderTranslator` | Before a translation request is sent to the provider | | [`TranslateEvent`](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-TranslationsManagement-AutoTranslate-Event-TranslateEvent.html) | `EventDispatchingProviderTranslator` | After a translation response is received | ## Side-by-side creation events Side-by-side creation events are dispatched when preparing a new translation draft. | Event | Dispatched by | Dispatched when | | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------ | ---------------------------------------------------------------- | | [`OnContentSideBySideTranslationCreateEvent`](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-TranslationsManagement-SideBySide-Event-OnContentSideBySideTranslationCreateEvent.html) | `ContentTranslationCreateController` | When creating a draft side-by-side translation of a content item | | [`OnProductSideBySideTranslationCreateEvent`](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-TranslationsManagement-SideBySide-Event-OnProductSideBySideTranslationCreateEvent.html) | `ProductTranslationViewController` | When creating a draft side-by-side translation of a product | # Other events > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). Events that are triggered when working with bookmarks, notifications, settings, forms and others. ## Bookmarks The following events are dispatched when adding content items to bookmarks. | Event | Dispatched by | Properties | | --------------------------- | --------------------------------- | -------------------- | | `BeforeCreateBookmarkEvent` | `BookmarkService::createBookmark` | `Location $location` | | `CreateBookmarkEvent` | `BookmarkService::createBookmark` | `Location $location` | | `BeforeDeleteBookmarkEvent` | `BookmarkService::deleteBookmark` | `Location $location` | | `DeleteBookmarkEvent` | `BookmarkService::deleteBookmark` | `Location $location` | ## Notifications The following events refer to [notifications displayed in the user menu](https://doc.ibexa.co/en/saas/administration/back_office/notifications/#user-notifications). | Event | Dispatched by | Properties | | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------- | | [`BeforeCreateNotificationEvent`](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-Core-Repository-Events-Notification-BeforeCreateNotificationEvent.html) | [`NotificationService::createNotification`](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-Core-Repository-NotificationService.html#method_createNotification) | `CreateStruct $createStruct` `?Notification $notification` | | [`CreateNotificationEvent`](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-Core-Repository-Events-Notification-CreateNotificationEvent.html) | [`NotificationService::createNotification`](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-Core-Repository-NotificationService.html#method_createNotification) | `Notification $notification` `CreateStruct $createStruct` | | [`BeforeDeleteNotificationEvent`](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-Core-Repository-Events-Notification-BeforeDeleteNotificationEvent.html) | [`NotificationService::deleteNotification`](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-Core-Repository-NotificationService.html#method_deleteNotification) | `Notification $notification` | | [`DeleteNotificationEvent`](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-Core-Repository-Events-Notification-DeleteNotificationEvent.html) | [`NotificationService::deleteNotification`](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-Core-Repository-NotificationService.html#method_deleteNotification) | `Notification $notification` | | [`BeforeMarkNotificationAsReadEvent`](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-Core-Repository-Events-Notification-BeforeMarkNotificationAsReadEvent.html) | [`NotificationService::markNotificationAsRead`](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-Core-Repository-NotificationService.html#method_markNotificationAsRead) | `Notification $notification` | | [`MarkNotificationAsReadEvent`](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-Core-Repository-Events-Notification-MarkNotificationAsReadEvent.html) | [`NotificationService::markNotificationAsRead`](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-Core-Repository-NotificationService.html#method_markNotificationAsRead) | `Notification $notification` | | [`BeforeMarkNotificationAsUnreadEvent`](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-Core-Repository-Events-Notification-BeforeMarkNotificationAsUnreadEvent.html) | [`NotificationService::markNotificationAsUnread`](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-Core-Repository-NotificationService.html#method_markNotificationAsUnread) | `Notification $notification` | | [`MarkNotificationAsUnreadEvent`](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-Core-Repository-Events-Notification-MarkNotificationAsUnreadEvent.html) | [`NotificationService::markNotificationAsUnread`](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-Core-Repository-NotificationService.html#method_markNotificationAsUnread) | `Notification $notification` | ## Settings The following events refer to key/value application-wide settings in database. | Event | Dispatched by | Properties | | -------------------------- | ------------------------------- | ---------------------------------------------------------------------------------------- | | `BeforeCreateSettingEvent` | `SettingService::createSetting` | `SettingCreateStruct $settingCreateStruct` `?Setting $setting` | | `CreateSettingEvent` | `SettingService::createSetting` | `Setting $setting` `SettingCreateStruct $settingCreateStruct` | | `BeforeUpdateSettingEvent` | `SettingService::updateSetting` | `Setting $setting` `SettingUpdateStruct $settingUpdateStruct` `?Setting $updatedSetting` | | `UpdateSettingEvent` | `SettingService::updateSetting` | `Setting $updatedSetting` `Setting $setting` `SettingUpdateStruct $settingUpdateStruct` | | `BeforeDeleteSettingEvent` | `SettingService::deleteSetting` | `Setting $setting` | | `DeleteSettingEvent` | `SettingService::deleteSetting` | `Setting $setting` | ## User preferences The following events are dispatched when changing the user settings available in the user menu. | Event | Dispatched by | Properties | | ------------------------------ | ------------------------------------------ | ----------------------------------------------------- | | `BeforeSetUserPreferenceEvent` | `UserPreferenceService::setUserPreference` | `UserPreferenceSetStruct[] $userPreferenceSetStructs` | | `SetUserPreferenceEvent` | `UserPreferenceService::setUserPreference` | `UserPreferenceSetStruct[] $userPreferenceSetStructs` | ## DAM assets | Event | Dispatched by | Properties | | --------------------- | ---------------------------------------------------- | ------------------------------------------------------------------------------------------------------------ | | `PublishVersionEvent` | `PublishAssetEventDispatcher::emitPublishAssetEvent` | `Content $content` `Connector\Dam\AssetIdentifier $assetIdentifier` `Connector\Dam\AssetSource $assetSource` | ## Image Editor The following event is dispatched when the Image Editor optimizes an image. You can subscribe to it to customize the list of active image optimizers at runtime. For more information, see [Customizing image optimizers with an event](https://doc.ibexa.co/en/saas/content_management/images/images/#customizing-image-optimizers). | Event | Dispatched by | Properties | | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------- | ---------------------------------------------------- | | [`ConfigureImageOptimizersEvent`](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-ImageEditor-Event-ConfigureImageOptimizersEvent.html) | `SpatieChainOptimizer::` `optimize` | `array $optimizers` | ## Form Builder (Experience) | Event | Dispatched by | Properties | | ------------------------------- | -------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------ | | `FieldAttributeDefinitionEvent` | `FieldDefinitionFactory::getAttributesDefinitions` | `FieldAttributeDefinitionBuilder $definitionBuilder` `array $configuration` | | `FieldDefinitionEvent` | `FieldDefinitionFactory::getFieldDefinition` | `FieldDefinitionBuilder $definitionBuilder` `array $configuration` | | `FieldValidatorDefinitionEvent` | `FieldDefinitionFactory::getValidatorsDefinitions` | `FieldDefinitionBuilder $definitionBuilder` `array $configuration` | | `FormActionEvent` | `HandleFormSubmission::handleFormSubmission` | `ContentView $contentView` `Ibexa\Contracts\FormBuilder\FieldType\Model\Form $form` `string $action` `mixed $data` | | `FormSubmitEvent` | `HandleFormSubmission::handleFormSubmission` | `ContentView $contentView` `Ibexa\Contracts\FormBuilder\FieldType\Model\Form $form` `array $data` | # Administration # Administration > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). Administer and configure your Cohesivo installation. Administer and configure your Cohesivo installation. - [Admin panel](https://ez-systems-developer-documentation--3381.com.readthedocs.build/en/3381/administration/admin_panel/admin_panel/): Cohesivo back office contains managements options for permissions, users, languages, content types, and system information. - [Project organization](https://ez-systems-developer-documentation--3381.com.readthedocs.build/en/3381/administration/project_organization/project_organization/): A Cohesivo project follows Symfony's directory structure to organize files in the project. - [Configuration](https://ez-systems-developer-documentation--3381.com.readthedocs.build/en/3381/administration/configuration/configuration/): In Cohesivo you store and manage configuration in project files, typically in YAML format. - [Back office](https://ez-systems-developer-documentation--3381.com.readthedocs.build/en/3381/administration/back_office/back_office/): Back office holds the administrator and editor interface and allows creating, publishing and managing content, users, settings, and more. # Project organization > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). A Cohesivo project follows Symfony's directory structure to organize files in the project. Cohesivo is a Symfony application and follows the project structure used by Symfony. You can see an example of organizing a project in the [companion repository](https://github.com/ezsystems/ezplatform-ee-beginner-tutorial/tree/v3-master) for the [Beginner tutorial](https://doc.ibexa.co/en/saas/tutorials/page_and_form_tutorial/page_and_form_tutorial/index.md). ## PHP code The project's PHP code (for example, controllers or event listeners) should be placed in the `src` folder. Reusable libraries should be packaged so that they can be managed with Composer. ## Templates Project templates should go into the `templates` folder. They can then be referenced in code without any prefix, for example `templates/full/article.html.twig` can be referenced in Twig templates or PHP as `full/article.html.twig`. ## Assets Project assets should go into the `assets` folder. They can be referenced as relative to the root, for example `assets/js/script.js` can be referenced as `js/script.js` from templates. All project assets are accessible through the `assets` path. ## Configuration You project's configuration is placed in the respective files in `config/packages`. For more information, see [Configuration](https://doc.ibexa.co/en/saas/administration/configuration/configuration/index.md). ### Importing configuration from a bundle If you're keeping some of your code in a bundle, dealing with core bundle semantic configuration can be tedious if you maintain it in the main `config/packages/ibexa.yaml` configuration file. You can import configuration from a bundle by following the Symfony tutorial [How to Import Configuration Files/Resources](https://symfony.com/doc/7.4/service_container.html#service-container-imports-directive). ## Versioning a project The recommended method is to version the whole project repository. # Architecture > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). Cohesivo architecture is structured in multiple layers connected by APIs. Cohesivo architecture is based on the philosophy to **use APIs** that is maintained in the long term. This **makes upgrades easier and provides lossless couplings** between all parts of the architecture, at the same time improving the migration capabilities of the system. The structure of a Cohesivo app is based on the Symfony framework but content management functions rely on the public PHP API. Other applications integrate with Cohesivo via REST API, which also relies on the public PHP API. ![Architecture](https://doc.ibexa.co/en/saas/administration/img/architecture.png "Architecture") The architecture of Cohesivo is layered and uses clearly defined APIs between the layers. | Layer | Description | | --------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | [Back office](https://doc.ibexa.co/en/saas/administration/back_office/back_office_configuration/index.md) | Back office contains all the necessary parts to run the Cohesivo's back office interface. | | [REST API v2](https://doc.ibexa.co/en/saas/api/rest_api/rest_api_usage/rest_api_usage/index.md) | The REST API v2 enables you to interact with a Cohesivo installation through the HTTP protocol, following a REST interaction model. | | Business Logic | The business logic is defined in the kernel. This business logic is exposed to applications via an API. It is used to organize development of the user interface layer. | # Bundles > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). Cohesivo is composed of bundles containing different parts of the application. A bundle in Symfony (and Cohesivo) is a separate part of your application that implements a feature. You can create bundles yourself or make use of available open-source bundles. You can also reuse the bundles you create in other projects or share them with the community. Many Cohesivo functionalities are provided through separate bundles included in the installation. You can see the bundles that are automatically installed with Cohesivo in the respective `composer.json` files. For example, for Ibexa Headless, see the [JSON file on GitHub](https://github.com/ibexa/headless/blob/6.0/composer.json). ## Working with bundles All bundles containing built-in Cohesivo functionalities are installed automatically. Additionally, you can install community-developed bundles from [Cohesivo Packages.](https://developers.ibexa.co/packages) To learn how to create your own bundles, see [Symfony documentation on bundles](https://symfony.com/doc/7.4/bundles.html). ### Overriding third-party bundles When you use an external bundle, you can override its parts, such as templates or controllers. To do so, make use of [Symfony's bundle override mechanism](https://symfony.com/doc/7.4/bundles/override.html). When overriding files, the path inside your application has to correspond to the path inside the bundle. ### Removing bundles To remove a bundle (either one you created yourself, or an out-of-the-box one that you don't need), remove the bundle entry from `config/bundles.php`. ## Core packages > **Tip: Tip** > > Ibexa Open Source is composed of the core packages. | Bundle | Description | | ----------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ | | [ibexa/admin-ui](https://github.com/ibexa/admin-ui) | Back office interface | | [ibexa/admin-ui-assets](https://github.com/ibexa/admin-ui-assets) | Assets for the back office | | [ibexa/content-forms](https://github.com/ibexa/content-forms) | Form-based integration for the Symfony Forms into content and user objects in kernel | | [ibexa/core](https://github.com/ibexa/core) | Core of the Cohesivo application | | [ibexa/core-search](https://github.com/ibexa/core-search) | Search-related capabilities | | [ibexa/core-persistence](https://github.com/ibexa/core-persistence) | Core system persistence | | [ibexa/cron](https://github.com/ibexa/cron) | Cron package for use with the `ibexa:cron:run` command | | [ibexa/design-engine](https://github.com/ibexa/design-engine) | [Design fallback system](https://doc.ibexa.co/en/saas/templating/design_engine/design_engine/index.md) | | [ibexa/doctrine-schema](https://github.com/ibexa/doctrine-schema) | Basic abstraction layer for cross-DBMS schema import | | [ibexa/fieldtype-matrix](https://github.com/ibexa/fieldtype-matrix) | [Matrix field type](https://doc.ibexa.co/en/saas/content_management/field_types/field_type_reference/matrixfield/index.md) | | [ibexa/fieldtype-query](https://github.com/ibexa/fieldtype-query) | [Query field type](https://doc.ibexa.co/en/saas/content_management/field_types/field_type_reference/contentqueryfield/index.md) | | [ibexa/fieldtype-richtext](https://github.com/ibexa/fieldtype-richtext) | field type for supporting rich-formatted text stored in a structured XML format | | [ibexa/graphql](https://github.com/ibexa/graphql) | GraphQL server for Cohesivo | | [ibexa/http-cache](https://github.com/ibexa/http-cache) | [HTTP cache handling](https://doc.ibexa.co/en/saas/infrastructure_and_maintenance/cache/http_cache/http_cache/index.md), using multi tagging | | [ibexa/i18n](https://github.com/ibexa/i18n) | Centralized translations to ease synchronization with Crowdin | | [ibexa/messenger](https://github.com/ibexa/messenger) | [Background and asynchronous task processing](https://doc.ibexa.co/en/saas/infrastructure_and_maintenance/background_tasks/index.md) using Symfony Messenger | | [ibexa/notifications](https://github.com/ibexa/notifications) | Sending notifications to channels | | [ibexa/post-install](https://github.com/ibexa/post-install) | Apache and nginx templates | | [ibexa/rest](https://github.com/ibexa/rest) | REST API | | [ibexa/search](https://github.com/ibexa/search) | Common search functionalities | | [ibexa/solr](https://github.com/ibexa/solr) | [Solr-powered](https://solr.apache.org/) search handler | | [ibexa/standard-design](https://github.com/ibexa/standard-design) | Standard design and theme to be handled by `design-engine` | | [ibexa/system-info](https://github.com/ibexa/system-info) | Information about the system Cohesivo is running on | | [ibexa/twig-components](https://github.com/ibexa/twig-components) | [Twig Components](https://doc.ibexa.co/en/saas/templating/components/index.md) | | [ibexa/user](https://github.com/ibexa/user) | User management | ## Ibexa Headless packages | Bundle | Description | | ----------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | ibexa/oss | Core packages | | ibexa/calendar | Calendar tab with a calendar widget | | ibexa/connect | [Ibexa Connect](https://doc.ibexa.co/projects/connect/en/latest/) | | ibexa/connector-ai | Foundation for the [AI Actions](https://doc.ibexa.co/en/saas/ai/ai_actions/ai_actions/index.md) framework | | ibexa/connector-dam | Connector for DAM (Digital Asset Management) systems | | ibexa/connector-openai | Integrates the AI framework with [OpenAI](https://openai.com) | | ibexa/content-tree | Content tree functionality | | ibexa/elasticsearch | Integration with Elasticsearch search engine | | ibexa/fastly | Fastly support for `http-cache`, for use on Ibexa Cloud or standalone | | ibexa/headless-assets | Assets for the back office | | ibexa/icons | Icon set for the back office | | ibexa/image-editor | [Image Editor](https://doc.ibexa.co/en/saas/content_management/images/configure_image_editor/index.md) | | ibexa/installer | Provides the `ibexa:install` command | | ibexa/measurement | Measurement field type and measurement product catalog attribute | | ibexa/migrations | [Migration of repository data](https://doc.ibexa.co/en/saas/content_management/data_migration/data_migration/index.md) | | ibexa/oauth2-client | Authenticate user through a [third-party OAuth 2 server](https://doc.ibexa.co/en/saas/users/oauth_client/index.md), integration with [`knpuniversity/oauth2-client-bundle`](https://github.com/knpuniversity/oauth2-client-bundle) | | ibexa/oauth2-server | Configure Cohesivo to act as a [OAuth2 Server](https://doc.ibexa.co/en/saas/users/oauth_server/index.md) | | ibexa/product-catalog-date-time-attribute | Implementation of the [Date and Time attribute type](https://doc.ibexa.co/en/saas/product_catalog/attributes/date_and_time/index.md) | | ibexa/product-catalog-symbol-attribute | Implementation of the [Symbol attribute type](https://doc.ibexa.co/en/saas/product_catalog/attributes/symbol_attribute_type/index.md) | | ibexa/product-catalog | Product catalog functionality | | ibexa/scheduler | Date-based publishing functionality | | ibexa/seo | Search Engine Optimization (SEO) tool | | ibexa/taxonomy | Taxonomy functionality | | ibexa/tree-builder | Tree builder functionality | | ibexa/version-comparison | Enables comparing between two versions of the same field | | ibexa/workflow | Collaboration feature that enables you to send content draft to any user for a review or rewriting | ## Ibexa Experience packages | Bundle | Description | | ----------------------- | ------------------------------------------------------------------------------------------------------------ | | ibexa/headless | Metapackage for Symfony Flex-based Cohesivo Headless installation | | ibexa/activity-log | Customer portal and corporate accounts | | ibexa/corporate-account | Customer portal and corporate accounts | | ibexa/dashboard | [Customizable dashboard](https://doc.ibexa.co/en/saas/administration/dashboard/customize_dashboard/index.md) | | ibexa/fieldtype-address | Address handling field type | | ibexa/form-builder | Enables creating Form content items with multiple form fields | | ibexa/page-builder | Page editor | | ibexa/fieldtype-page | Page handling field type | | ibexa/permissions | Additional permission functionalities | | ibexa/segmentation | Segment functionality for profiling the content displayed to specific users | | ibexa/site-factory | Enables configuration of sites from UI | | ibexa/engage | Enables integration with [Qualifio](https://developers.qualifio.com/docs/engage/) | ## Optional packages The following packages are optional and can be installed independently. | Bundle | Description | | --------- | ------------------------------------------------------------------------------------------ | | ibexa/cdp | Integration with [Raptor CDP](https://doc.ibexa.co/en/saas/raptor_cdp/raptor_cdp/index.md) | In addition, you can extend the capabilities of your project by installing additional [LTS Updates](https://doc.ibexa.co/en/saas/ibexa_products/editions/#lts-updates). # Configure default dashboard > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). Configure default dashboard. Editions: Experience You can configure default dashboard under the `ibexa.system..admin_group` [configuration key](https://doc.ibexa.co/en/saas/administration/configuration/configuration/#configuration-files). Create `ibexa_dashboard.yaml` file in the `config/packages/` directory. The following example configuration defines default dashboard: ```yaml ibexa: system: admin_group: dashboard: container_remote_id: dashboard_container default_dashboard_remote_id: default_dashboard users_container_remote_id: user_dashboards predefined_container_remote_id: predefined_dashboards section_identifier: dashboard content_type_identifier: dashboard_landing_page container_content_type_identifier: folder ``` Configuration can be set per [SiteAccess](https://doc.ibexa.co/en/saas/multisite/multisite_configuration/#siteaccess-configuration) or [SiteAccess group](https://doc.ibexa.co/en/saas/multisite/multisite_configuration/#siteaccess-groups). All the settings in the configuration are reflected in the back office. ## Container remote ID Defines starting location container for all the dashboards, including customized and predefined ones. You can see it in the **Admin** panel, **Dashboards** section, **Dashboards** folder in the content tree. In the **Technical details** tab, it is defined as **Location remote ID**. ![Container remote ID](https://doc.ibexa.co/en/saas/administration/img/dashboard_container_remote_id.png) ## Default dashboard remote ID Specifies default predefined dashboard. All the users can see this dashboard as a starting dashboard in the back office. You can see it in the **Admin** panel, **Dashboards** section, **Default dashboard** folder inside of **Predefined dashboards** container in the content tree. In the **Technical details** tab, it's defined as **Location remote ID**. ## Users container remote ID Defines a container for users folders, which contain all customized dashboards. You can see it in the **Admin** panel, **Dashboards** section, **User dashboards** folder inside of main **Dashboards** container in the content tree. In the **Technical details** tab, it's defined as **Location remote ID**. ## Predefined container remote ID Defines a container that contains all predefined dashboards created by Administrator. You can see it in the **Admin** panel, **Dashboards** section, **Predefined dashboards** folder inside of main **Dashboards** container in the content tree. In the **Technical details** tab, it's defined as **Location remote ID**. ## Section identifier Specifies the name of the [Section](https://doc.ibexa.co/en/saas/administration/content_organization/sections/index.md). ## Content type identifier It is an identifier that represents dashboard content type. You can find it in the **Admin** panel, **Dashboard content Type** section, **View/Global properties** tab. ![Content type identifier](https://doc.ibexa.co/en/saas/administration/img/dashboard_content_type_identifier.png) ## Container content type identifier Determines the content type identifier of the container for dashboards and lets you create additional structure for the predefined dashboards. By default all the dashboards containers are set as a folders. ![Container content type](https://doc.ibexa.co/en/saas/administration/img/dashboard_container_type.png) If the `folder` content type doesn't exist or is modified, you can use another one, for example: ```yaml ibexa: system: default: dashboard: container_content_type_identifier: user_dashboard_container ``` The custom content type should be a container and needs to have a field type with `name` identifier. # Customize dashboard > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). Customize dashboard. Editions: Experience > **Note: Note** > > The Dashboard Builder is available only in the Experience edition. The dashboard from the Headless edition can be customized using [Twig Components](https://doc.ibexa.co/en/saas/templating/components/index.md). You can customize the dashboard depending on your needs using Dashboard Builder. Customized dashboard displays a set of widgets selected by the user. > **Tip: Tip** > > For detailed instruction on how to customize dashboards with the Dashboard Builder, see [User Documentation](https://doc.ibexa.co/projects/userguide/en/6.0/getting_started/dashboard/work_with_dashboard/#customize-dashboard). ## Manage permissions To customize dashboard, you need to have `dashboard/customize` [policy](https://doc.ibexa.co/en/saas/permissions/permission_overview/index.md). By default, all the users belonging to the `Editors` user group, have `Dashboard`[role](https://doc.ibexa.co/en/saas/administration/admin_panel/roles_admin_panel/index.md) assigned, so they can edit, create, or delete dashboard. If, by any reason, you want to narrow this permission, you can set up specific [limitations](https://doc.ibexa.co/en/saas/permissions/limitations/index.md). ## Add custom layout For new dashboard you need to choose layout which defines the available zones. While opening Dashboard Builder, layout window appears - you can choose one from available layouts. You can also add custom layout that then can be available in Dashboard Builder. ## Create custom blocks Dashboard Builder provides set of ready-to-use blocks, for example, Common content, Quick actions, or Cohesivo News. For more information about available blocks, see [User Documentation](https://doc.ibexa.co/projects/userguide/en/6.0/getting_started/dashboard/dashboard_block_reference/). In addition to existing blocks available in Dashboard Builder, you can also create custom blocks. To do it, follow the instruction on how to [create custom page block](https://doc.ibexa.co/en/saas/content_management/pages/create_custom_page_block/index.md). # DashboardService's PHP API > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). Use DashboardService to manage dashboards. Editions: Experience You can use `DashboardService`'s PHP API to manage custom dashboards. To obtain this service, inject the `Ibexa\Contracts\Dashboard\DashboardServiceInterface`. The service exposes two functions: - `createCustomDashboardDraft(?Location $location = null): Content` - returns a new content item in draft state of `dashboard` content type. If no location is given, it creates a copy of the dashboard of the user currently logged in. If a location is given, it creates a copy with the given location. The default name of the customized dashboard is set as `My dashboard`. This new Content draft is located in the current user custom dashboard container. - `createDashboard(DashboardCreateStruct $dashboardCreateStruct): Content` - publishes the given dashboard creation structure (`Ibexa\Contracts\Dashboard\Values\DashboardCreateStruct`) under `dashboard.predefined_container_remote_id`. ## Customize dashboard using DashboardService The following example is a command deploying a custom dashboard to users of content groups. Using the `admin` account, it loads the group members, logs each one in, creates a custom dashboard by copying a default one, and then publishes the draft version of customized dashboard. First argument is the `Content ID` of the dashboard to copy. Following arguments are the Content IDs of the user groups. ```php locationService = $repository->getLocationService(); $this->contentService = $repository->getContentService(); $this->userService = $repository->getUserService(); $this->permissionResolver = $repository->getPermissionResolver(); parent::__construct(); } public function configure(): void { $this ->addArgument('dashboard', InputArgument::REQUIRED, 'Location ID of the dashboard model') ->addArgument('group', InputArgument::REQUIRED | InputArgument::IS_ARRAY, 'User Group Content ID(s)'); } protected function execute(InputInterface $input, OutputInterface $output): int { $dashboardModelLocationId = (int)$input->getArgument('dashboard'); $userGroupLocationIdList = array_map(intval(...), $input->getArgument('group')); foreach ($userGroupLocationIdList as $userGroupLocationId) { try { $admin = $this->userService->loadUserByLogin('admin'); $this->permissionResolver->setCurrentUserReference($admin); foreach ($this->userService->loadUsersOfUserGroup($this->userService->loadUserGroup($userGroupLocationId)) as $user) { $this->permissionResolver->setCurrentUserReference($user); $dashboardDraft = $this->dashboardService->createCustomDashboardDraft($this->locationService->loadLocation($dashboardModelLocationId)); $this->contentService->publishVersion($dashboardDraft->getVersionInfo()); } } catch (\Throwable $throwable) { dump($throwable); } } return self::SUCCESS; } } ``` The following line runs the command with `74` as the model dashboard's Content ID, `13` the user group's Content ID, and on the SiteAccess `admin` to have the right `user_content_type_identifier` config: ```bash php bin/console doc:dashboard 74 13 --siteaccess=admin ``` # Admin panel > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). Cohesivo back office contains managements options for permissions, users, languages, content types, and system information. Once you set up your environment you can start your work as an administrator. You can find key tools in **Admin** panel. To access **Admin** panel, click the icon: ![Admin panel Icon](https://doc.ibexa.co/en/saas/administration/img/admin_panel_icon.png). - [Users](https://ez-systems-developer-documentation--3381.com.readthedocs.build/en/3381/administration/admin_panel/users_admin_panel/): You can access all users and user groups in the Users tab. - [Roles](https://ez-systems-developer-documentation--3381.com.readthedocs.build/en/3381/administration/admin_panel/roles_admin_panel/): To give users an access to your website you need to assign them roles in the Admin Panel. - [URL Management](https://ez-systems-developer-documentation--3381.com.readthedocs.build/en/3381/administration/admin_panel/url_management_admin_panel/): URL Management lets you manage external URL addresses and URL wildcards. - [Languages](https://ez-systems-developer-documentation--3381.com.readthedocs.build/en/3381/administration/admin_panel/languages_admin_panel/): Cohesivo offers the ability to create multiple translations of your website. - [Segments](https://ez-systems-developer-documentation--3381.com.readthedocs.build/en/3381/administration/admin_panel/segments_admin_panel/): You can use segments to display specific content to specific users. - [Corporate](https://ez-systems-developer-documentation--3381.com.readthedocs.build/en/3381/administration/admin_panel/corporate_admin_panel/): You can manage companies profiles in the Admin Panel. - [Workflow](https://ez-systems-developer-documentation--3381.com.readthedocs.build/en/3381/administration/admin_panel/workflow_admin_panel/): The workflow functionality passes a content item version through a series of stages. - [System Information](https://ez-systems-developer-documentation--3381.com.readthedocs.build/en/3381/administration/admin_panel/system_information_admin_panel/): System information provides basic system information such as versions of all installed packages. # Users > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). You can access all users and user groups in the Users tab. [Users](https://doc.ibexa.co/en/saas/users/users/index.md) in Cohesivo are treated the same way as content items. They're organized in groups such as *Guests*, *Editors*, *Anonymous*, which makes it easier to manage them and their permissions. You can access all users and user groups in the **Admin** panel by selecting **Users**. ![Users and user groups](https://doc.ibexa.co/en/saas/administration/img/admin_panel_users.png "Users and user groups") > **Caution: Caution** > > Be careful not to delete an existing user account. If you do this, content created by this user can be broken and the application can face malfunction. # Roles > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). To give users an access to your website you need to assign them roles in the Admin Panel. To give users an access to your website you need to assign them roles in the **Admin** panel. ![Roles](https://doc.ibexa.co/en/saas/administration/img/admin_panel_roles.png "Roles") Each role consists of: ## Policies ![Policies](https://doc.ibexa.co/en/saas/administration/img/admin_panel_policies.png "Policies") Policies are the rules that give users access to different function in a module. You can restrict what user can do with limitations. The available limitations depend on the chosen policy. When policy has more than one limitation, all of them have to apply. See [example use case](https://doc.ibexa.co/en/saas/permissions/permission_use_cases/#restrict-editing-to-part-of-the-tree). > **Note: Note** > > Limitation specifies what a user can do, not what they can't do. A `Location` limitation, for example, gives the user access to content with a specific location, not prohibits it. > > For more information, see [Limitation reference](https://doc.ibexa.co/en/saas/permissions/limitation_reference/index.md). ## Assignments ![Assignments](https://doc.ibexa.co/en/saas/administration/img/admin_panel_assignments.png "Assignments") After you created all policies, you can assign the role to users and/or user groups with possible additional limitations. Every user or user group can have multiple roles. A user can also belong to many groups, for example, Administrators, Editors, Subscribers. Best practice is to avoid assigning roles to users directly. Model your content (for example, content types, sections, or locations) in a way that can be accessed by generic roles. That way system is be more secure and easier to manage. This approach also improves performance. Role assignments and policies are taken into account during search/load queries. For more information, see [Permissions overview](https://doc.ibexa.co/en/saas/permissions/permissions/index.md) and [Permission use cases](https://doc.ibexa.co/en/saas/permissions/permission_use_cases/index.md). # URL Management > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). URL Management lets you manage external URL addresses and URL wildcards. You can manage external URL addresses and URL wildcards in the **Admin** panel. Configure URL aliases to have human-readable URL addresses throughout your system. For more information, see [URL management](https://doc.ibexa.co/en/saas/content_management/url_management/url_management/index.md). ![URL Management](https://doc.ibexa.co/en/saas/administration/img/admin_panel_url_management.png "URL Management") # Languages > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). Cohesivo offers the ability to create multiple translations of your website. Cohesivo offers the ability to create multiple translations of your website. Which version is shown to a visitor depends on the way your installation is set up. You can add a new language version for the website in the [Admin Panel](https://doc.ibexa.co/en/saas/administration/admin_panel/admin_panel/index.md) in the **Languages** tab. Every new language must have a name and a language code, written in the `xxx-XX` format, for example `eng-GB`. ![Languages](https://doc.ibexa.co/en/saas/administration/img/admin_panel_languages.png "Languages") The multilanguage system operates based on a global translation list that contains all languages available in the installation. After adding a language you may have to reload the application to be able to use it. Depending on your set up, additional configuration may be necessary for the new language to work properly, especially with SiteAccesses. See [Languages](https://doc.ibexa.co/en/saas/multisite/languages/languages/index.md) for further information. # Segments > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). You can use segments to display specific content to specific users. Editions: Experience You can use segments to display specific content to specific [users](https://doc.ibexa.co/en/saas/users/users/index.md). They're used out of the box in the Targeting block in the page. You can collect segments in segment groups: ![Segment groups](https://doc.ibexa.co/en/saas/administration/img/admin_panel_segment_groups.png) Each segment group can contain segments that you can target content for. ![Segment](https://doc.ibexa.co/en/saas/administration/img/admin_panel_segment.png) You can assign users to segments [through the API](https://doc.ibexa.co/en/saas/users/segment_api/#assigning-users). # Corporate > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). You can manage companies profiles in the Admin Panel. Editions: Experience You can manage companies profiles in the **Admin** panel. There, in the **Corporate** section, you can find basic information about existing companies, for example, details, versions, locations, translations, a list of members, billing addresses, and technical details regarding the organization, such as visibility, IDs, or relations. ![Corporate section](https://doc.ibexa.co/en/saas/administration/img/admin_panel_corporate.png "Corporate section") For more information, see [Customer management](https://doc.ibexa.co/projects/userguide/en/6.0/customer_management/manage_customers/). # Workflow > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). The workflow functionality passes a content item version through a series of stages. The workflow functionality passes a content item version through a series of stages. Each workflow consists of stages and transitions between them. For more information, see [Workflow](https://doc.ibexa.co/en/saas/content_management/workflow/workflow/index.md). ![Workflow](https://doc.ibexa.co/en/saas/administration/img/admin_panel_workflow.png "Workflow") # System Information > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). System information provides basic system information such as versions of all installed packages. The System Information panel in the back office is sourced in the [`ibexa/system-info` repository](https://github.com/ibexa/system-info). There you can also find basic system information such as versions of all installed packages. ![System Information](https://doc.ibexa.co/en/saas/administration/img/admin_panel_system_info.png "System Information") # Sections > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). Sections are used to divide content items in the tree. Sections are used to divide content items in the tree into groups that are more manageable by content editors. Division into sections allows you, among others, to set [permissions](https://doc.ibexa.co/en/saas/permissions/permission_overview/index.md) for only a part of the tree. ![Sections screen](https://doc.ibexa.co/en/saas/administration/img/admin_panel_sections.png "Sections screen") Technically, a section is a number, a name, and an identifier. Content items are placed in sections by being assigned the section ID. One item can be in only one section. When a new content item is created, its section ID is set to the default section (which is usually Standard). When the item is published it is assigned to the same section as its parent. Because content must always be in a section, unassigning happens by choosing a different section to move it into. If a content item has multiple location assignments then it is always the section ID of the item referenced by the parent of the main location that is used. In addition, if the main location of a content item with multiple location assignments is changed then the section ID of that item is updated. When content is moved to a different location, the item itself and all of its subtree are assigned to the section of the new location. It works only for copy and move. Assigning a new section to a parent content item doesn't affect the subtree, meaning that subtree cannot currently be updated this way. Sections can only be removed if no content items are assigned to them. Even then, it should be done carefully. When a section is deleted, it's only its definition itself that is removed. Other references to the section remain and thus the system most likely loses consistency. > **Caution: Caution** > > Removing sections may corrupt permission settings, template output and other things in the system. Section ID numbers aren't recycled. If a section is removed, its ID number cannot be reused when a new section is created. # Content types > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). A content type is a base for new content items. A content type is a base for new content items. It defines what fields are available in the content item. ![Content types](https://doc.ibexa.co/en/saas/administration/img/admin_panel_content_types.png "Content types") For example, a new content type called *Article* can have fields such as title, author, body, or image. Based on this content type, you can create any number of content items. Content types are organized into groups. ![Content type groups](https://doc.ibexa.co/en/saas/administration/img/admin_panel_content_type_groups.png "Content type groups") You can add your own groups here to keep your content types in better order. For a full tutorial, see [Add a content type](https://doc.ibexa.co/en/saas/getting_started/first_steps/#add-a-content-type) or follow [User Documentation](https://doc.ibexa.co/projects/userguide/en/6.0/content_management/create_edit_content_types/). For a detailed overview of the content model, see [Content model overview](https://doc.ibexa.co/en/saas/content_management/content_model/index.md). ## Content type metadata Each content type is characterized by a set of metadata which define the general behavior of its instances: **Name** – a user-friendly name that describes the content type. This name is used in the interface, but not internally by the system. It can consist of letters, digits, spaces, and special characters (it's mandatory and the maximum length is 255 characters). > **Note: Note** > > Even if your content type defines a field intended as a name for the content item (for example, a title of an article or product name), don't confuse it with this Name, which is a piece of metadata, not a field. **Identifier** – an identifier for internal use in configuration, for example, files, templates, or PHP code. It must be unique, can only contain lowercase letters, digits, and underscores (it's mandatory and the maximum length is 50 characters). **Description** – a detailed description of the content type (optional). **Content name pattern** – a pattern that defines what name a new content item based on this content type gets. The pattern usually consists of field identifiers that tell the system which fields it should use when generating the name of a content item. Each field identifier has to be surrounded with angle brackets. Text outside the angle brackets is included literally. If no pattern is provided, the system automatically uses the first field (optional). **URL alias name pattern** – a pattern which controls how the virtual URLs of the locations are generated when content items are created based on this content type. Only the last part of the virtual URL is affected. The pattern works in the same way as the content name pattern. Text outside the angle brackets is converted with the selected method of URL transformation. If no pattern is provided, the system automatically uses the name of the content item itself (optional). > **Tip: Changing URL alias and content name patterns** > > If you change the content name pattern or the URL alias name pattern, existing content items cannot be modified automatically. The new pattern is only applied after you modify the content item and save a new version. > > The old URL aliases continue to redirect to the same content items. **Container** – a flag which indicates if content items based on this content type are allowed to have sub-items or not (mainly relevant for actions via the UI, not validated by every PHP API). > **Note: Note** > > This flag was added for convenience and only affects the interface. In other words, it doesn't control any actual low-level logic, it simply controls the way the graphical user interface behaves. **Sort children by default by** – rule for sorting sub-items. If the instances of this content type can serve as containers, their children are sorted according to what is selected here. **Sort children by default in order** – another rule for sorting sub-items. This decides the sort order for the criterion chosen above. **Make content available even with missing translations** – a flag which indicates if content items of this content type should be available even without a corresponding language version. See [Content availability](https://doc.ibexa.co/en/saas/content_management/content_availability/index.md). ![Creating a new content type](https://doc.ibexa.co/en/saas/content_management/img/admin_panel_new_content_type.png) ## Field definitions Aside from the metadata, a content type may contain any number of field definitions (but has to contain at least one). They determine what fields of what field types are included in all content items based on this content type. ![Field definitions](https://doc.ibexa.co/en/saas/administration/img/admin_panel_field_definitions.png) ![Diagram of an example content type](https://doc.ibexa.co/en/saas/content_management/img/content_model_type_diagram.png) > **Note: Note** > > You can assign each field defined in a content type to a group by selecting one of the groups in the Category drop-down. [Available groups can be configured in the content repository](https://doc.ibexa.co/en/saas/administration/configuration/repository_configuration/index.md). > **Caution: Caution** > > In case of content types containing many field types you should be aware of possible memory-related issues with publishing/editing. They're caused by the limitation of how many `$_POST` input variables can be accepted. > > The easiest way to fix them is by increasing the `max_input_vars` value in the `php.ini` configuration file. This solution isn't universally recommended and you're proceeding on your own risk. > > Setting the limit inappropriately may damage your project or cause other issues. You may also experience performance problems with such large content types, in particular when you have many content items. If you're experincing too many issues, consider rearranging your project to avoid them. ## Modifying content types A content type and its field definitions can be modified after creation, even if there are already content items based on it in the system. When a content type is modified, each of its instances are changed as well. If a new field definition is added to a content type, this field appears (empty) in every relevant content item. If a field definition is deleted from the content type, all the corresponding fields are removed from content items of this type. ## Removing content types System content types are by default used for the File Uploads and removing them can cause errors. If you decide to remove a `file` or `image` content type, or change their identifiers, you need to change the configuration, so it reflects the available content types. Example configuration: ```yaml parameters: ibexa.multifile_upload.location.default_mappings: # Image - mime_types: - image/jpeg - image/jpg - image/pjpeg - image/pjpg - image/png - image/bmp - image/gif - image/tiff - image/x-icon - image/webp content_type_identifier: custom_image_contenttype content_field_identifier: image name_field_identifier: name # File - mime_types: - image/svg+xml - application/msword - application/vnd.openxmlformats-officedocument.wordprocessingml.document - application/vnd.ms-excel - application/vnd.openxmlformats-officedocument.spreadsheetml.sheet - application/vnd.ms-powerpoint - application/vnd.openxmlformats-officedocument.presentationml.presentation - application/pdf content_type_identifier: custom_file_contenttype content_field_identifier: file name_field_identifier: name ibexa.multifile_upload.fallback_content_type: content_type_identifier: custom_file_contenttype content_field_identifier: file name_field_identifier: name ``` # Object states > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). Object states are user-defined states that can be assigned to content items. Object states are user-defined states that can be assigned to content items. They're contained in groups. ![Object State group](https://doc.ibexa.co/en/saas/administration/img/admin_panel_object_state_groups.png "Object state group") If a state group contains any states, each content item is automatically assigned a state from this group. You can assign states to content in the back office in the content item's **Technical details** tab. ![Assigning an object state to a content item](https://doc.ibexa.co/en/saas/administration/img/assigning_an_object_state.png "Assigning an object state to a content item") By default, Cohesivo contains one object state group: **Lock**, with states **Locked** and **Not locked**. ![Lock Object state](https://doc.ibexa.co/en/saas/administration/img/object_state_lock.png "Lock object state") Object states can be used in conjunction with [permissions](https://doc.ibexa.co/en/saas/permissions/permission_overview/index.md), in particular with the [object state limitation](https://doc.ibexa.co/en/saas/permissions/limitation_reference/#object-state-limitation). Their specific use cases depend on your needs and the setup of your permission system. # Configuration > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). In Cohesivo you store and manage configuration in project files, typically in YAML format. Cohesivo configuration is delivered by means of a number of dedicated configuration files. It contains everything from selecting the content repository to SiteAccesses to language settings. ## Configuration format The recommended configuration format is YAML. It's used by default in the kernel (and in examples throughout the documentation). However, you can also use XML or PHP formats for configuration. ## Configuration files Configuration files are located in the `config` folder. Configuration is provided per package in the `config/packages` folder, and routes are defined per package in `config/routes`. `config/packages/ibexa.yaml` contains basic configuration. It stores, among others, [SiteAccess](https://doc.ibexa.co/en/saas/multisite/multisite/index.md) information and content view config. Other configuration is provided in respective files, for example, `config/packages/ibexa_admin_ui.yaml`, `config/packages/ibexa_http_cache.yaml`. You can make configuration environment-specific by using separate folders for each environment. These files contain additional settings and point to the general (not environment-specific) configuration that is applied in other cases. > **Note: New configuration files** > > It's good practice to provide your own configuration in separate files. Any YAML files placed in the `config/packages` folder is automatically included in the system configuration. > **Tip: Tip** > > Read more about [how configuration is handled in Symfony](https://symfony.com/doc/7.4/best_practices.html#configuration). > **Caution: Special characters** > > Avoid using special characters in your configuration files. More specifically, don't use Unicode characters from the ["Other" (`C`) categories](https://en.wikipedia.org/wiki/Unicode#General_Category_property), such as control or format characters. > > Make sure your IDE displays them. > > Be careful when copy-pasting text from a word processing software or a PDF, because it might contain hidden characters like the [soft hyphen](https://en.wikipedia.org/wiki/Soft_hyphen). ## Configuration handling > **Note: Note** > > Configuration is tightly related to the [service container](https://doc.ibexa.co/en/saas/api/php_api/php_api/#service-container). To fully understand it, you must be familiar with the service container and [its configuration](https://symfony.com/doc/7.4/service_container.html#service-container-parameters). Basic configuration handling in Cohesivo is similar to what is commonly possible with Symfony. You can define key/value pairs in your configuration files. Internally and by convention, keys follow a *dot syntax*, where the different segments follow your configuration hierarchy. Keys are usually prefixed by a *namespace* corresponding to your application. All kinds of values are accepted, including arrays and deep hashes. For configuration that is meant to be exposed to an end-user (or end-developer), it's usually a good idea to also [implement semantic configuration](https://symfony.com/doc/7.4/components/config/definition.html). You can also [implement SiteAccess-aware semantic configuration](https://doc.ibexa.co/en/saas/multisite/siteaccess/siteaccess_aware_configuration/index.md). For example: ```yaml parameters: myapp.parameter.name: someValue myapp.boolean.param: true myapp.some.hash: foo: bar an_array: [apple, banana, pear] ``` ```php // Usage inside a controller /** @var \Symfony\Component\DependencyInjection\ContainerInterface $container */ $myParameter = $container->getParameter('myapp.parameter.name'); ``` ## Configuration settings For specific configuration settings, see: - [Back office configuration](https://doc.ibexa.co/en/saas/administration/back_office/back_office_configuration/index.md) - [Repository configuration](https://doc.ibexa.co/en/saas/administration/configuration/repository_configuration/index.md) - [Content views](https://doc.ibexa.co/en/saas/templating/templates/template_configuration/index.md) - [Multisite configuration](https://doc.ibexa.co/en/saas/multisite/multisite_configuration/index.md) - [Image variations](https://doc.ibexa.co/en/saas/content_management/images/images/#configuring-image-variations) - [Logging and debug](https://doc.ibexa.co/en/saas/infrastructure_and_maintenance/devops/#logging-and-debug-configuration) - [Authentication](https://doc.ibexa.co/en/saas/infrastructure_and_maintenance/security/development_security/#symfony-authentication) - [Sessions](https://doc.ibexa.co/en/saas/infrastructure_and_maintenance/sessions/#configuration) - [Persistence cache](https://doc.ibexa.co/en/saas/infrastructure_and_maintenance/cache/persistence_cache/#persistence-cache-configuration) # Dynamic configuration > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). Use the ConfigResolver to inject dynamic configuration into your services. ## ConfigResolver Dynamic configuration is handled by the [`ConfigResolverInterface`](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-Core-SiteAccess-ConfigResolverInterface.html). It exposes the `hasParameter()` and `getParameter()` methods. You can use them to check the different *scopes* available for a given *namespace* to find the appropriate parameter. To work with the ConfigResolver, your dynamic settings must have the following name format: `..parameter.name`. ```yaml parameters: # Internal configuration ibexa.site_access.config.default.content.default_ttl: 60 ibexa.site_access.config.site_group.content.default_ttl: 3600 # Here "myapp" is the namespace, followed by the SiteAccess name as the parameter scope # Parameter "my_param" will have a different value in site_group and admin_group myapp.site_group.my_param: value myapp.admin_group.my_param: another value # Defining a default value, for other SiteAccesses myapp.default.my_param: Default value ``` Inside a controller extending the `Ibexa\Core\MVC\Symfony\Controller\Controller` class, in `site_group` SiteAccess, you can use the parameters in the following way (the same applies for `hasParameter()`): ```php $configResolver = $this->getConfigResolver(); // ibexa.site_access.config is the default namespace, so no need to specify it // The following will resolve ibexa.site_access.config..content.default_ttl // In the case of site_group, it will return 3600. // Otherwise it will return the value for ibexa.site_access.config.default.content.default_ttl (60) $locationViewSetting = $configResolver->getParameter( 'content.default_ttl' ); // For you own namespace, you need to specify it, here as "myapp" $myParamSetting = $configResolver->getParameter( 'my_param', 'myapp' ); // $myParamSetting's value will be 'value'   // You can also force the scope by naming it explicitly (here as "admin_group") $myParamSettingAdmin = $configResolver->getParameter( 'my_param', 'myapp', 'admin_group' ); // $myParamSetting's value will be 'another value' ``` > **Tip: Tip** > > To learn more about scopes, see [SiteAccess documentation](https://doc.ibexa.co/en/saas/multisite/multisite_configuration/#scope). Both `getParameter()` and `hasParameter()` can take three arguments: 1. `$paramName` - the name of the parameter 2. `$namespace` - your application namespace, `myapp` in the previous example. If null, the default namespace is used, which is `ibexa.site_access.config` by default. 3. `$scope` - a SiteAccess name. If null, the current SiteAccess is used. ## Inject ConfigResolver into services You can use the ConfigResolver in your own services whenever needed. To do this, inject the `ibexa.config.resolver` service: ```yaml services: App\Service: arguments: ['@ibexa.config.resolver'] ``` You can also use the [autowire feature](https://symfony.com/doc/7.4/service_container/autowiring.html), by type hinting against [`ConfigResolverInterface`](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-Core-SiteAccess-ConfigResolverInterface.html). For more information about dependency injection, see [Service container](https://doc.ibexa.co/en/saas/api/php_api/php_api/#service-container). > **Note: Note** > > Don't store the retrieved config value unless you know what you're doing. SiteAccess can change during code execution, which means you might work on the wrong value. ```php namespace App; use Ibexa\Contracts\Core\SiteAccess\ConfigResolverInterface; class Service { public function __construct(private readonly ConfigResolverInterface $configResolver) { } public function someMethodThatNeedConfig(): void { $configValue = $this->configResolver->getParameter('my_param', 'myapp'); } } ``` # Repository configuration > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). Configure repository connections, archive limits, field groups and other settings. You can define several repositories within a single application. However, you can only use one per site. ## Repository connection ### Using default values To use the default repository connection, you don't need to specify its details: ```yaml ibexa: repositories: # Defining Repository with alias "main" # Default storage engine is used, with default connection # Equals to: # main: { storage: { engine: legacy, connection: } } main: ~ ``` > **Note: Legacy storage engine** > > Legacy storage engine is the default storage engine for the repository. > > It uses [Doctrine DBAL](https://www.doctrine-project.org/projects/doctrine-dbal/en/latest/) (Database Abstraction Layer). Database settings are supplied by [DoctrineBundle](https://github.com/doctrine/DoctrineBundle). As such, you can refer to [DoctrineBundle's documentation](https://github.com/doctrine/DoctrineBundle/blob/2.19.x/docs/en/configuration.rst#doctrine-dbal-configuration). If no repository is specified for a SiteAccess or SiteAccess group, the first repository defined under `ibexa.repositories` is used: ```yaml ibexa: repositories: main: ~ system: # All members of site_group will use "main" Repository # No need to set "repository", it will take the first defined Repository by default site_group: # ... ``` #### Multisite URI matching with multi-repository setup You can use only one repository (database) per domain. This doesn't prohibit using [different repositories](https://doc.ibexa.co/en/saas/infrastructure_and_maintenance/cache/persistence_cache/#multi-repository-setup) on different subdomains. However, when you use URI matching for multisite setup, all SiteAccesses sharing domain also need to share repository. For example: - `ibexa.co` domain can use `ibexa_repo` - `doc.ibexa.co` domain can use `doc_repo` But the following configuration would be invalid: - `ibexa.co` domain can use `ibexa_repo` - `ibexa.co/doc` **cannot** use `doc_repo`, as it's under the same domain. Invalid configuration causes problems for different parts of the system, for example, back-end UI, REST interface, and other non-SiteAccess-aware Symfony routes such as `/_fos_user_context_hash` used by [HTTP cache](https://doc.ibexa.co/en/saas/infrastructure_and_maintenance/cache/http_cache/http_cache/index.md). ### Entity manager If you use the [Doctrine entity manager](https://www.doctrine-project.org/projects/doctrine-orm/en/2.18/tutorials/getting-started.html#obtaining-the-entitymanager), you're unable to connect different SiteAccesses to different databases. To have this possibility, you need to use the SiteAccess-aware entity manager: `ibexa.doctrine.orm.entity_manager`. To inject your entities into the SiteAccess-aware entity manager, use the following configuration: ```yaml ibexa: orm: entity_mappings: IbexaCoreBundle: is_bundle: true type: attribute dir: Entity prefix: Ibexa\Bundle\Core\Entity ``` For more information, see [DoctrineBundle documentation](https://symfony.com/doc/7.4/reference/configuration/doctrine.html). > **Note: Note** > > In contrast with DoctrineBundle, when you use the SiteAccess-aware entity manager you need to explicitly set all options: `dir` (it still accepts relative path in case of bundles), `prefix`, `type`, and `is_bundle`. ### Defining custom connection You can also explicitly define a custom repository connection: ```yaml doctrine: dbal: default_connection: my_connection_name connections: my_connection_name: driver: pdo_mysql host: localhost port: 3306 dbname: my_database user: my_user password: my_password charset: UTF8MB4 my_second_connection_name: driver: pdo_mysql url: '%env(resolve:SECOND_DATABASE_URL)%' charset: UTF8MB4 another_connection_name: # ... ibexa: repositories: first_repository: storage: engine: legacy connection: my_connection_name config: {} # Configuring search is required when using Legacy search engine search: connection: my_connection_name second_repository: storage: engine: legacy connection: my_second_connection_name config: {} search: connection: my_second_connection_name another_repository: storage: engine: legacy connection: another_connection_name config: {} search: connection: another_connection_name # ... system: my_first_siteaccess: repository: first_repository my_second_siteaccess: repository: second_repository ``` ```bash # .env.local SECOND_DATABASE_URL=otherdb://otheruser:otherpasswd@otherhost:otherport/otherdbname?otherdbserversion ``` ## Field groups configuration Field groups, used in content and content type editing, can be configured under the `repositories` key. Values entered there are field group *identifiers*: ```yaml repositories: default: fields_groups: list: [content, features, metadata] default: content ``` These identifiers can be given human-readable values and can be translated. Those values are used when editing content types. The translation domain is `ibexa_fields_groups`. This example in `translations/ibexa_fields_groups.en.yaml` defines English names for field groups: ```yaml content: Content metadata: Metadata user_data: User data ``` ## Limit of archived content versions `default_version_archive_limit` controls the number of archived versions per content item that are stored in the repository. By default it's set to 5. This setting is configured in the following way (typically in `ibexa.yaml`): ```yaml ibexa: repositories: default: options: default_version_archive_limit: 10 ``` This limit is enforced on publishing a new version and only covers archived versions, not drafts. > **Tip: Tip** > > Don't set `default_version_archive_limit` too high. In Legacy storage engine you can see performance degradation if you store too many versions. The default value of 5 is the recommended value, but the less content you have overall, the more you can increase this to, for instance, 25 or even 50. ### Grace period for archived versions After a new version of a content item is published, the previous version, now archived, can still be loaded for a certain period of time, using the same permission set as the published version. This period is called the grace period and it prevents race conditions that can occur when a new version is published at the same time as someone is accessing the content item. The duration can be configured using the `grace_period_in_seconds` setting. After a version has been archived for longer than specified in the configuration, the grace period ends and the version is treated the same as all the other archived versions, including the need of [`content/versionread` policy](https://doc.ibexa.co/en/saas/permissions/policies/#content) to access it. ```yaml ibexa: repositories: default: options: grace_period_in_seconds: 30 ``` `grace_period_in_seconds` uses the [PHP's `max_execution_time`](https://www.php.net/manual/en/info.configuration.php#ini.max-execution-time) value by default. Set the value to 0 to disable grace period for archived versions. ### Removing versions on publication With `remove_archived_versions_on_publish` setting, you can control whether versions that exceed the limit are deleted when you publish a new version. ```yaml ibexa: repositories: default: options: remove_archived_versions_on_publish: true ``` `remove_archived_versions_on_publish` is set to `true` by default. Set it to `false` if you have multiple older versions of content and need to avoid performance drops when publishing. When you set the value to `false`, run [`ibexa:content:cleanup-versions`](#removing-old-versions) periodically to make sure that content item versions that exceed the limit are removed. ### Removing old versions You can use the `ibexa:content:cleanup-versions` command to remove old content versions. The command takes the following optional parameters: - `status` or `t` - status of versions to remove: `draft`, `archived` or `all` - `keep` or `k` - number of versions to keep - `user` or `u` - the User that the command is performed as. The user must have the `content/remove`, `content/read` and `content/versionread` policies. By default the `administrator` user is applied. - `excluded-content-types` - exclude versions of one or multiple content types from the cleanup procedure. Separate multiple content types identifiers with the comma. `ibexa:content:cleanup-versions --status --keep --user --excluded-content-types article,blog_post` For example, the following command removes archived versions as user `admin`, but leaves the 5 most recent versions: `ibexa:content:cleanup-versions --status archived --keep 5 --user administrator` ## User identifiers `ibexa_default_settings.yaml` contains two settings that indicate which content types are treated like users and user groups: ```yaml ibexa: system: default: user_content_type_identifier: [user] user_group_content_type_identifier: [user_group] ``` You can override these settings if you have other content types that should be treated as users/user groups in the back office. When viewing such content in the back office you're able to see, for example, the assigned policies. ## Top-level Locations You can change the default path for top-level locations such as content or media in the back office, for example: ```yaml ibexa: system: : subtree_paths: content: '/1/18/' media: '/1/57/' ``` ## Content Scheduler snapshots Content Scheduler snapshots speed up the rendering of Content Scheduler blocks and reduce the space used in the database. By default, five snapshots are stored, but you can modify this number with the following configuration, depending on the complexity of the Content Scheduler blocks: ```yaml parameters: ibexa.field_type.page.block.schedule.snapshots.amount: 10 ``` ## Repository-aware configuration In your custom development, you can create repository-aware configuration settings. This enables you to use different settings for different repositories. > **Tip: SiteAccess-aware configuration** > > If you need to use different settings per SiteAccess, not per repository, see [SiteAccess-aware configuration](https://doc.ibexa.co/en/saas/multisite/siteaccess/siteaccess_aware_configuration/index.md). To do this, create a parser that implements `Ibexa\Bundle\Core\DependencyInjection\Configuration\RepositoryConfigParserInterface`: ```php use Ibexa\Bundle\Core\DependencyInjection\Configuration\RepositoryConfigParserInterface; use Symfony\Component\Config\Definition\Builder\NodeBuilder; final class CustomRepositoryConfigParser implements RepositoryConfigParserInterface { public function addSemanticConfig(NodeBuilder $nodeBuilder): void { $nodeBuilder ->arrayNode('acme') ->children() ->scalarNode('my_setting') ->isRequired() ->defaultValue(120) ->end() ->end() ->end(); } } ``` You need to register this configuration extension in the following way: ```php use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\HttpKernel\Bundle\Bundle; final class AcmeFeatureBundle extends Bundle { public function build(ContainerBuilder $container): void { // ... /** @var Ibexa\Bundle\Core\DependencyInjection\IbexaCoreExtension $kernel */ $kernel = $container->getExtension('ibexa'); $kernel->addRepositoryConfigParser(new CustomRepositoryConfigParser()); } } ``` To access the configuration settings, use the `Ibexa\Bundle\Core\ApiLoader\RepositoryConfigurationProvider::getRepositoryConfig` method: ```php /** @var \Ibexa\Contracts\Core\Container\ApiLoader\RepositoryConfigurationProviderInterface $repositoryConfigProvider */ $acmeConfig = $repositoryConfigProvider->getRepositoryConfig()['acme']; ``` # Back office > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). Back office holds the administrator and editor interface and allows creating, publishing and managing content, users, settings, and more. The back office interface is produced by the [`ibexa/admin-ui` bundle](https://github.com/ibexa/admin-ui). Additionally, it uses React-based modules that make each part of the UI extensible, and Bootstrap for styling. The interface is accessible in your browser at `http:///admin`. To extend the back office with PHP code, you can use [events](https://symfony.com/doc/7.4/event_dispatcher.html), either built-in Symfony events or events dispatched by the application. Some extensibility, such as [adding custom tags](https://doc.ibexa.co/en/saas/content_management/rich_text/extend_online_editor/#configure-custom-tags), is possible without writing your own code, with configuration and templating only. > **Note: String translations** > > Refer to [Custom string translations](https://doc.ibexa.co/en/saas/multisite/languages/back_office_translations/#custom-string-translations) to learn how to provide string translations when extending the back office. - [Back office configuration](https://ez-systems-developer-documentation--3381.com.readthedocs.build/en/3381/administration/back_office/back_office_configuration/): Configure default upload locations, pagination limits, and more settings for the back office. - [Back office menus](https://ez-systems-developer-documentation--3381.com.readthedocs.build/en/3381/administration/back_office/back_office_menus/back_office_menus/): All menus in the back office are based on KnpMenuBundle and you can easily extend them with new items. - [Back office tabs](https://ez-systems-developer-documentation--3381.com.readthedocs.build/en/3381/administration/back_office/back_office_tabs/back_office_tabs/): Tabs are used for content view, in dashboard, system information and other parts of the back office and are extensible. - [Reusable components](https://ez-systems-developer-documentation--3381.com.readthedocs.build/en/3381/administration/back_office/back_office_elements/reusable_components/): Speed up creating back office templates with the help of ready-made reusable components. - [Notifications](https://ez-systems-developer-documentation--3381.com.readthedocs.build/en/3381/administration/back_office/notifications/): You can send notifications to users who work with the back office by using notification bars or notifications in the user menu. - [Browser](https://ez-systems-developer-documentation--3381.com.readthedocs.build/en/3381/administration/back_office/browser/browser/): Customize the configuration of the content browser. - [Add user setting](https://ez-systems-developer-documentation--3381.com.readthedocs.build/en/3381/administration/back_office/add_user_setting/): Add the option to select a custom preference in user menu. - [Customize calendar](https://ez-systems-developer-documentation--3381.com.readthedocs.build/en/3381/administration/back_office/customize_calendar/): Add custom events to the calendar and customize its looks. # Back office configuration > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). Configure default upload locations, pagination limits, and more settings for the back office. ## Pagination limits Default pagination limits for different sections of the back office can be defined through respective settings in [`ezplatform_default_settings.yaml`](https://github.com/ibexa/admin-ui/blob/6.0/src/bundle/Resources/config/ezplatform_default_settings.yaml#L7). You can set the pagination limit for user settings under the `ibexa.system..pagination_user` [configuration key](https://doc.ibexa.co/en/saas/administration/configuration/configuration/#configuration-files): ```yaml ibexa: system: : pagination_user: user_settings_limit: 6 ``` You can configure the following settings to manage the pagination limits for the product catalog: ```yaml ibexa: system: : product_catalog: pagination: attribute_definitions_limit: 10 attribute_groups_limit: 10 currencies_limit: 10 customer_groups_limit: 10 customer_group_users_limit: 10 products_limit: 10 product_types_limit: 10 product_view_custom_prices_limit: 10 regions_limit: 10 catalogs_limit: 10 ``` ## Subtree operations ### Copy subtree limit Copying large subtrees can cause performance issues, so you can limit the number of content items that can be copied at once by setting the `ibexa.system..subtree_operations.copy_subtree.limit` [configuration key](https://doc.ibexa.co/en/saas/administration/configuration/configuration/#configuration-files). The limit applies only to the UI of the back office and disables the "Copy subtree" operation. The default value is `100`. You can set it to `-1` for no limit, or to `0` to completely disable copying subtrees. To copy a subtree regardless of the limit, use the following console command: ```bash php bin/console ibexa:copy-subtree ``` ### Query subtree limit When working with large content trees, counting child items or calculating subtree sizes can cause significant performance degradation due to unbounded database queries. You can limit these count operations by setting the `ibexa.system..subtree_operations.query_subtree.limit` [configuration key](https://doc.ibexa.co/en/saas/administration/configuration/configuration/#configuration-files): ```yaml ibexa: system: : subtree_operations: copy_subtree: limit: 100 query_subtree: limit: 500 ``` The default value for `query_subtree.limit` is `500`. You can set it to `-1` to disable the limit. This limit applies in some cases when the back office needs to determine if a location has children or calculate the number of items in a subtree. The limit does not affect the sub-items list, which still displays all child elements in a paginated way. When a limit is set, the query stops after finding the specified number of items instead of performing a full count. This significantly improves performance on locations with large numbers of children. The resulting count is displayed with a `+` sign, indicating that the result is not exact. ![Example of subtree count with exceeded limit](https://doc.ibexa.co/en/saas/administration/back_office/img/query_subtree_limit_locations_tab.png "Example of subtree count with exceeded limit") ## Default locations Default location IDs for [content structure, Media, and users](https://doc.ibexa.co/en/saas/content_management/locations/#top-level-locations) in the menu are configured with the `ibexa.system..location_ids` [configuration key](https://doc.ibexa.co/en/saas/administration/configuration/configuration/#configuration-files): ```yaml ibexa: system: : location_ids: content_structure: 2 media: 43 users: 5 ``` # Content tree > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). Configure SiteAccess, displayed content items, depth and root location for the content tree. With this configuration you can: - define configuration for a SiteAccess or a SiteAccess group - decide how many content items are displayed in the tree - set maximum depth of expanded tree - hide content types - set a tree root location - override content tree's root for specific locations ```yaml ibexa: system: # any SiteAccess or SiteAccess group admin_group: content_tree_module: # defines how many children are shown after expanding parent load_more_limit: 15 # users won't be able to load more children than that children_load_max_limit: 200 # maximum depth of expanded tree tree_max_depth: 10 # content types to display in content tree, value of '*' allows all CTs to be displayed allowed_content_types: '*' # content tree won't display these content types, can be used only when 'allowed_content_types' is set to '*' ignored_content_types: - post - article # ID of Location to use as tree root. If omitted - content.tree_root.location_id setting is used. tree_root_location_id: 2 # list of Location IDs for which content tree's root Location is changed contextual_tree_root_location_ids: - 2 # Home (Content structure) - 5 # Users - 43 # Media ``` # Reusable components > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). Speed up creating back office templates with the help of ready-made reusable components. When you extend the back office, you can use base Twig templates for commonly used UI components such as tables or tabs. The available templates are: - `@ibexadesign/ui/component/alert/alert.html.twig` - `@ibexadesign/ui/component/table/table.html.twig` - `@ibexadesign/ui/component/tab/tabs.html.twig` To use the components, [`embed`](https://twig.symfony.com/doc/3.x/tags/embed.html) them in templates. With `embed` you can override blocks that are defined inside the included template. ## Alerts The alert component has the following properties: - `type` - available types of alert: error, info, success, and warning - `icon` - name of the icon, taken from the default icon set - `icon_path` - full icon path, in case you don't want to use an icon from the default icon set - `title` - alert title - `subtitle` - displays subtitle content - `show_subtitle_below` - default set to `false`, the subtitle is displayed next to the title - `extra_content` - use to add custom elements, such as buttons or additional text - `show_close_btn` - by default set to `false`, if set to `true`, the **Close** button is displayed but requires additional JavaScript configuration on your side to work - `is_toast` - default set to `false`, applies the toast design - `class` - additional CSS classes - `attributes` - additional HTML attributes ```html+twig {% include '@ibexadesign/ui/component/alert/alert.html.twig' with { type: 'info', title: 'Some title', subtitle: 'Some subtitle', show_subtitle_below: true, icon_path: ibexa_icon_path('visibility-hidden'), class: 'mb-4', } only %} ``` ## Details The details component consists of the following blocks: - `details_header` - `details_items` Variables: | Name | Type | Values | | --------------------- | ------ | ------------------------------------------- | | `headline` (optional) | string | if not specified, the header isn't rendered | | `headline_items` | array | | | `view_mode` | string | `vertical`, default set to `''` | | `items` | hash | {`label`, `content_raw`, `content`} | If `headline` isn't specified, the `headline_items` isn't rendered. ## Modal The modal component consists of the following blocks: ```html+twig {% block dialog %} {% block content_before %} {% block content %} {% block header %} {% block subheader %} {% block body %} {% block footer %} {% block content_after %} ``` Variables: | Name | Type | Values | | --------------------- | ------- | ---------------------------------------------------------------- | | `size` | string | `small`, `large`, `extra-large`, default set to: `''` | | `subtitle` | string | no default value, if not defined, the `subheader` isn't rendered | | `no_header` | boolean | default set to `false` | | `no_header_border` | boolean | default set to `false` | | `class` | string | default `''` | | `id` | string | | | `has_static_backdrop` | boolean | default set to `false` | `attr` and other `attr_*` hold all HTML attributes rendered on their respective elements. `attr` | Name | Type | Values | | ---------- | ------ | ---------------- | | `class` | string | default `''` | | `role` | string | default `dialog` | | `tabindex` | string | default `-1` | `attr_dialog` | Name | Type | Values | | ------- | ------ | ------------------------- | | `class` | string | default set to `''` | | `role` | string | default set to `document` | `attr_content` | Name | Type | Values | | ------- | ------ | ------------------- | | `class` | string | default set to `''` | `attr_title` | Name | Type | Values | | ------- | ------ | ------------------- | | `class` | string | default set to `''` | `attr_close_btn` | Name | Type | Values | | ------- | ------ | ----------------------- | | `class` | string | default set to `''` | | `type` | string | default set to `button` | | `title` | string | default set to `Close` | ## Tables The table component consists of the following blocks: - `header` - headline for the table section - `headline` - table name - `actions` - action buttons, for example, create, bulk delete - `table` - the table itself - `thead` - table header content - `tbody` - table body content ### Override specific cell For the `twig` table component to have full control over rendering the rows of specific cells, only data are passed to it. Data rows are passed in an array - one row per one array element. It is necessary to put objects with the columns data in an array. There are a few types of table columns: - normal content column - `{ content: col_name }` - a column icon - `{ has_icon: true, content: col_icon }` - a checkbox column - `{ has_checkbox: true, content: col_checkbox }` - action buttons column - `{ has_action_btns: true, content: col_action_btns }` Each column has the `raw` parameter which prevents the component from the escaping content (untrusted user-generated content). If you want to create an array based on some data from the backend, create an empty array and fill it with items (which corresponds to table rows) inside for loop: ```html+twig {% set body_rows = [] %} {% for article in pager.currentPageResults %} {# we may render checkbox using form_widget or just put HTML #} {% set col_checkbox %} {{ form_widget(form_remove.articles[article.id]) }} {% endset %} ​ {% set col_icon %} {% endset %} ``` ### Render hyperlink The following example shows how to render both text and hyperlink which redirect to the specified content. ```html+twig {% set col_name %} {{ ibexa_content_name(article.contentInfo) }} {% endset %} {% set col_action_btns %} {% if article.userCanEdit %} {% endif %} {% endset %} {% set body_rows = body_rows|merge([{ cols: [ { has_checkbox: true, content: col_checkbox }, { has_icon: true, content: col_icon }, { content: col_name }, { content: article.contentType.name }, { has_action_btns: true, content: col_action_btns }, ]}]) %} {% endfor %} ``` ### Actions See the example below to learn how to create an action button which removes the article in the table. The table component has to be wrapped into the remove article form. As in many cases you want a button to be disabled when no item in a table is selected and enabled otherwise, there is a built-in mechanism for this. To enable it you need to add the `ibexa-toggle-btn-state` CSS class to the form element alongside `data-toggle-button-id` data-attribute which holds the id of the button that should be enabled/disabled after a checkbox state change. Next, pass a button under the `action` parameter to the table headline. Action buttons are rendered on the right side of the table headline (don't confuse it with the table header). You can also specify headline text, which is a table title displayed above, by passing it under `headline` parameter. You can generate various headline texts by using the `results_headline` macro with a few parameters: - `count` - of all results, not only displayed on the first page - `has_filters` - when using filters - `phrase` - filtering phrase - `results_headline` - ensures the headlines consistency across the platform - `head_cols` - an array for table header (not headline), corresponds with consecutive column Column types available for the table header : - normal content column `{ content: col_name }` (content is the title of the column) - icon column `{ has_icon: true }` - checkbox column `{ has_checkbox: true }` - action buttons column `{ }` Additional parameters available for all of the objects mentioned earlier: ```text - class (CSS class) - attr (HTML attributes) ``` See the example: ```html+twig { content: 'foo', class: 'bar', attr: { colspan: 2, }, ``` - `empty_table_info_text` and `empty_table_action_text` specify texts which are displayed when the table is empty. ```html+twig {{ form_start(form_remove, { action: path('ibexa.article.remove'), attr: { class: 'ibexa-toggle-btn-state', 'data-toggle-button-id': '#article_remove_remove' } }) }} {% include '@ibexadesign/ui/component/table/table.html.twig' with { headline: results_headline(pager.getNbResults()), head_cols: [ { has_checkbox: true }, { has_icon: true }, { content: 'article.list.name'|trans|desc('Name') }, { content: 'article.list.content_type'|trans|desc('Content type') }, { }, ], body_rows, actions: form_widget(form_remove.remove, { attr: { class: 'btn ibexa-btn ibexa-btn--ghost ibexa-btn--small', disabled: true, }}), empty_table_info_text: 'article.list.empty'|trans|desc('You have no articles yet. Your articles will show up here.'), empty_table_action_text: 'article.list.empty_desc'|trans|desc('Articles you create will show up here.'), } %} {{ form_end(form_remove) }} ``` Other table component parameters include: - `class` - (CSS table class) - `attr` - (other HTML attributes applied on the HTML table element), for example: - `attr: { 'data-some-data-attribute-you-need': 'foo' }` - `table_body_class` and `table_body_attr` are the same as mentioned earlier, but applied on the table element - `show_head_cols_if_empty` - (default: `false`), by default, when `body_rows` is empty, the table component doesn't show the table header, but you may want to have it because for example rows are rendered dynamically with JavaScript on the browser side. To avoid wrapping headline inside the form, as it's done in the earlier example, you can `embed` table and override the `between_header_and_table` block: ```html+twig {% block between_header_and_table %} {{ form_start(form_remove, { action: path('ibexa.article.remove'), attr: { class: 'ibexa-toggle-btn-state', 'data-toggle-button-id': '#article_remove_remove' } }) }} {% endblock %} ``` This method is practical in case of another form inside headline actions or to avoid interferences with the form like button triggering its submission. By default, tables are wrapped in a scrollable wrapper which prevents them from being too long. To disable it, set the `is_scrollable` parameter to `false`. > **Tip: Tip** > > For an example of using the table component, see [Add menu item](https://doc.ibexa.co/en/saas/administration/back_office/back_office_menus/add_menu_item/index.md). ## Tabs The tab component consists of the following block: - `tab_content` - tab content The tab component supports the following variables: - `tabs` - `id` - tab ID - `label` - human-readable label for the tab - `active` - true if tab is active - `content` - HTML content of tab if `tab_content` isn't overridden - `tab_content_class` - additional CSS classes attached to `.tab-content` - `tab_content_attributes` - additional HTML attributes added to `.tab-content` ```html+twig {% embed '@ibexadesign/ui/component/tab/tabs.html.twig' with { tabs: [ { id: 'first', label: 'First' }, { id: 'second', label: 'Second' }, ] } %} {% block tab_content %} {% embed '@ibexadesign/ui/component/tab/tab_pane.html.twig' with { id: 'first', active: true } %} {% block content %} First {% endblock %} {% endembed %} {% embed '@ibexadesign/ui/component/tab/tab_pane.html.twig' with { id: 'second' } %} {% block content %} Second.

Some Rich HTML content

{% endblock %} {% endembed %} {% endblock %} {% endembed %} ``` With tabs, you can use [`include`](https://twig.symfony.com/doc/3.x/tags/include.html) instead of `embed` when you pass tab content as a variable while rendering the template: ```html+twig {% include '@ibexadesign/ui/component/tab/tabs.html.twig' with { tabs: [ { id: 'first', label: 'First', content: 'First tab content' }, { id: 'second', label: 'Second', content: 'Second tab content', active: true }, ] } %} ``` # Add drop-downs > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). Add custom drop down menus to back office interface. In Cohesivo, you can create a reusable custom drop-down and implement it anywhere in the back office. Follow the steps below to learn how to integrate this component to fit it to your project needs. ## Create `` input, for example: ```twig {% set source %} {% endset %} ``` `` input header. | | `choices` | | Elements listed in the drop-down. | | `preferred_choices` | | Elements listed at the top of the list with a separator. | | `value` | - | The currently selected element. It is an object with a key `value`. | | `multiple` | true false | Boolean. To allow users to select multiple items. | | `translation_domain` | true false | Used for translating choices and placeholder. | | `custom_form` | true false | For custom form must be set to true. | | `class` | | Additional classes for the element with `ibexa-dropdown` class. | | `placeholder` | | Placeholder displayed when no option is selected. | | `custom_init` | true false | By default set to `false`. If set to `true`, requires manually initializing drop-down in JavaScript. | | `is_disabled` | true false | Disables drop-down. | | `is_hidden` | true false | Hides the whole widget. | | `is_small` | true false | Adjusts height of the widget (from 48px to 32px). | | `is_ghost` | true false | Changes layout of the widget, removes all borders and backgrounds (similar to buttons modifier). | | `min_search_items` | number, default 5 | Minimum number of options that have to be passed to show the search inside the drop-down. | | `selected_item_label` | text | Allows setting constant label for widget. By default the visible label shows the currently selected options. | | `has_select_all_toggler` | true false | Allows showing a "Select all" option if the minimum number of items is reached. | | `min_select_all_toggler_items` | number, default 5 | Minimum number of items the dropdown must have for the "Select all" option to appear. | ![Drop-down expanded state](https://doc.ibexa.co/en/saas/administration/img/dropdown_expanded_state.png) ## Extend drop-down templates ### Initialize All drop-downs are searched and initialized automatically in `admin.dropdown.js`. To extend or modify the search, you need to add a `custom_init` attribute to the drop-down Twig parameters. Otherwise it's initialized two times. Next, run the following JavaScript code: ```javascript (function (global, document) { const container = document.querySelector('.ibexa-dropdown'); const dropdown = new global.ibexa.core.Dropdown({ container, selectorSource, }); dropdown.init(); })(window, window.document); ``` ## Configuration options Full list of options: | Name | Description | Required | | ---------------- | ----------------------------------------------------------------------------- | -------- | | `container` | Contains a reference to a DOM node where the custom drop-down is initialized. | required | | `selectorSource` | Use to change class of the source element. | required | # Custom icons > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). Configure custom icons to use for content types. ## Customize content type icons To add custom icons for existing content types or custom content types in Cohesivo, use the following configuration under the `ibexa.system..content_type` [configuration key](https://doc.ibexa.co/en/saas/administration/configuration/configuration/#configuration-files): ```yaml ibexa: system: default: content_type: article: thumbnail: /assets/images/custom_icon.svg#custom category: thumbnail: /bundles/ibexaadminuiassets/vendors/ids-assets/dist/img/all-icons.svg#folder ``` Place the icon in `public/assets/images` and run `yarn encore ` after adding it. > **Note: Icons format** > > To ensure proper display in the back office, all icons should have SVG format with `symbol`. Use the [scope](https://doc.ibexa.co/en/saas/multisite/multisite_configuration/#scope) if you want different icons for different SiteAccesses. # Add drag and drop > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). Add custom drag-and-drop interactions to back office interface. You can create a generic interface for drag and drop interactions that you can reuse in many places across the back office. First, prepare the HTML code structure and place it in a Twig template. See the example: ```html
item name
item name
item name
``` To initialize a drag and drop interface, add a JavaScript Code that comes with the template following the convention: ```javascript (function (global, doc, ibexa) { const draggable = new ibexa.core.Draggable({ itemsContainer: doc.querySelector('.items-container-drag'), selectorItem: '.item-drag', selectorPlaceholder: '.item-placeholder-drag', }); draggable.init(); })(window, window.document, window.ibexa); ``` For more information on creating Twig templates, see [Templating basics](https://doc.ibexa.co/en/saas/templating/templates/templates/index.md). ## Configuration options Full list of options: | Option | Description | Required | | --------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------- | | `itemsContainer` | Reference to DOM node that contains a draggable item. | required | | `selectorItem` | CSS selector of a draggable item. | required | | `selectorPlaceholder` | CSS selector of a placeholder. | required | | `afterInit` | Callback function invoked after interface initialization. | optional | | `afterDragStart` | Callback function invoked after starting to drag. | optional | | `afterDragOver` | Callback function invoked after moving onto a droppable element. | optional | | `afterDrop` | Callback function invoked after dropping an element. | optional | | `attachCustomEventHandlersToItem` | Function to be invoked while attaching event handlers to every item in the item's container. Item of `HTMLElement` type is passed to the function as the first param. | optional | | `timeoutRemovePlaceholders` | The amount of time after which the not dropped item disappears.The default value is set to 500ms. | optional | # Customizing the back office with Twig Components > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). Back office components allow you to inject any custom widgets into selected places of the user interface. You can customize many of the back office views by using [Twig components](https://doc.ibexa.co/en/saas/templating/components/index.md). This allows you to inject your own custom logic and extend the templates. The available groups for the back office are: ## Admin UI | Group name | Template file | | ----------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `admin-ui-login-form-after` | `vendor/ibexa/admin-ui/src/bundle/Resources/views/themes/admin/account/login/index.html.twig` | | `admin-ui-login-form-before` | `vendor/ibexa/admin-ui/src/bundle/Resources/views/themes/admin/account/login/index.html.twig` | | `admin-ui-content-column-end` | `vendor/ibexa/admin-ui/src/bundle/Resources/views/themes/admin/ui/layout.html.twig` | | `admin-ui-content-create-form-before` | - `vendor/ibexa/admin-ui-ui/src/bundle/Resources/views/themes/admin/content/create/create.html.twig` - `vendor/ibexa/admin-ui-ui/src/bundle/Resources/views/themes/admin/ui/on_the_fly/create_on_the_fly.html.twig` - `vendor/ibexa/admin-ui-ui/src/bundle/Resources/views/themes/admin/user/create.html.twig` | | `admin-ui-content-create-form-after` | - `vendor/ibexa/admin-ui-ui/src/bundle/Resources/views/themes/admin/content/create/create.html.twig` - `vendor/ibexa/admin-ui-ui/src/bundle/Resources/views/themes/admin/ui/on_the_fly/create_on_the_fly.html.twig` - `vendor/ibexa/admin-ui-ui/src/bundle/Resources/views/themes/admin/user/create.html.twig` | | `admin-ui-content-edit-form-after` | - `vendor/ibexa/admin-ui-ui/src/bundle/Resources/views/themes/admin/content/edit/edit.html.twig` - `vendor/ibexa/admin-ui-ui/src/bundle/Resources/views/themes/admin/ui/on_the_fly/edit_on_the_fly.html.twig` - `vendor/ibexa/admin-ui-ui/src/bundle/Resources/views/themes/admin/user/edit.html.twig` | | `admin-ui-content-edit-form-before` | - `vendor/ibexa/admin-ui-ui/src/bundle/Resources/views/themes/admin/content/edit/edit.html.twig` - `vendor/ibexa/admin-ui-ui/src/bundle/Resources/views/themes/admin/ui/on_the_fly/edit_on_the_fly.html.twig` - `vendor/ibexa/admin-ui-ui/src/bundle/Resources/views/themes/admin/user/edit.html.twig` | | `admin-ui-content-edit-sections` | - `vendor/ibexa/admin-ui-ui/src/bundle/Resources/views/themes/admin/content/edit/edit.html.twig` - `vendor/ibexa/admin-ui-ui/src/bundle/Resources/views/themes/admin/ui/on_the_fly/create_on_the_fly.html.twig` - `vendor/ibexa/admin-ui-ui/src/bundle/Resources/views/themes/admin/ui/on_the_fly/edit_on_the_fly.html.twig` | | `admin-ui-content-form-create-header-actions` | `vendor/ibexa/admin-ui-ui/src/bundle/Resources/views/themes/admin/content/create/create.html.twig` | | `admin-ui-content-form-edit-header-actions` | `vendor/ibexa/admin-ui-ui/src/bundle/Resources/views/themes/admin/content/edit/edit.html.twig` | | `admin-ui-content-translations-row-actions` | `vendor/ibexa/admin-ui/src/bundle/Resources/views/themes/admin/content/tab/translations/tab.html.twig` | | `admin-ui-content-tree-after` | `vendor/ibexa/admin-ui/src/bundle/Resources/views/themes/admin/content/location_view.html.twig` | | `admin-ui-content-tree-before` | `vendor/ibexaadmin-ui/src/bundle/Resources/views/themes/admin/content/location_view.html.twig` | | `admin-ui-content-type-edit-sections` | `vendor/ibexa/admin-ui-ui/src/bundle/Resources/views/themes/admin/content_type/edit.html.twig` | | `admin-ui-content-type-tab-groups` | `vendor/ibexa/admin-ui-ui/src/bundle/Resources/views/themes/admin/content_type/index.html.twig` | | `admin-ui-dashboard-all-tab-groups` | `vendor/ibexa/admin-ui-ui/src/bundle/Resources/views/themes/admin/ui/dashboard/block/all.html.twig` | | `admin-ui-dashboard-blocks` | `vendor/ibexa/admin-ui-ui/src/bundle/Resources/views/themes/admin/ui/dashboard/dashboard.html.twig` | | `admin-ui-dashboard-my-tab-groups` | `vendor/ibexa/admin-ui-ui/src/bundle/Resources/views/themes/admin/ui/dashboard/block/me.html.twig` | | `admin-ui-distraction-free-mode-extras` | `vendor/ibexa/admin-ui-ui/src/bundle/Resources/views/themes/admin/ui/form_fields.html.twig` | | `admin-ui-form-content-add-translation-body` | `vendor/ibexa/admin-ui-ui/src/bundle/Resources/views/themes/admin/content/modal/add_translation.html.twig` | | `admin-ui-global-search-autocomplete-templates` | `vendor/ibexa/admin-ui-ui/src/bundle/Resources/views/themes/admin/ui/global_search.html.twig` | | `admin-ui-global-search` | `vendor/ibexa/admin-ui-ui/src/bundle/Resources/views/themes/admin/ui/layout.html.twig` | | `admin-ui-header-user-menu-middle` | `vendor/ibexa/admin-ui-ui/src/bundle/Resources/views/themes/admin/ui/menu/user.html.twig` | | `admin-ui-image-edit-actions-after` | - `vendor/ibexa/admin-ui-ui/src/bundle/Resources/views/themes/admin/ui/field_type/edit/ibexa_image.html.twig` - `vendor/ibexa/admin-ui-ui/src/bundle/Resources/views/themes/admin/ui/field_type/edit/ibexa_image_asset.html.twig` | | `admin-ui-layout-content-after` | `vendor/ibexa/admin-ui-ui/src/bundle/Resources/views/themes/admin/ui/layout.html.twig` | | `admin-ui-link-manager-block` | `vendor/ibexa/admin-ui-ui/src/bundle/Resources/views/themes/admin/url_management/url_management.html.twig` | | `admin-ui-location-view-content-alerts` | `vendor/ibexa/admin-ui/src/bundle/Resources/views/themes/admin/content/location_view.html.twig` | | `admin-ui-location-view-tab-groups` | `vendor/ibexa/admin-ui/src/bundle/Resources/views/themes/admin/content/location_view.html.twig` | | `admin-ui-location-view-tabs-after` | `vendor/ibexa/admin-ui-ui/src/bundle/Resources/views/themes/admin/ui/tab/location_view.html.twig` | | `admin-ui-script-body` | `vendor/ibexa/admin-ui-ui/src/bundle/Resources/views/themes/admin/ui/layout.html.twig` | | `admin-ui-script-head` | `vendor/ibexa/admin-ui-ui/src/bundle/Resources/views/themes/admin/ui/layout.html.twig` | | `admin-ui-stylesheet-body` | - `vendor/ibexa/admin-ui-ui/src/bundle/Resources/views/themes/admin/ui/layout_error.html.twig` - `vendor/ibexa/admin-ui-ui/src/bundle/Resources/views/themes/admin/ui/layout.html.twig` | | `admin-ui-stylesheet-head` | `vendor/ibexa/admin-ui-ui/src/bundle/Resources/views/themes/admin/ui/layout.html.twig` | | `admin-ui-systeminfo-tab-groups` | `vendor/ibexa/system-info/src/bundle/Resources/views/themes/admin/system_info/info.html.twig` | | `admin-ui-user-menu` | `vendor/ibexa/admin-ui-ui/src/bundle/Resources/views/themes/admin/ui/layout.html.twig` | | `admin-ui-user-profile-blocks` | `vendor/ibexa/admin-ui/src/bundle/Resources/views/themes/admin/account/profile/view.html.twig` | | `admin-ui-versions-table-before` | `vendor/ibexa/admin-ui/src/bundle/Resources/views/themes/admin/content/tab/versions/table.html.twig` | For more information, see [this example using few of those components](https://doc.ibexa.co/en/saas/templating/components/#example). ## Calendar | Group name | Template file | | --------------------------------- | --------------------------------------------------------------------------------------- | | `admin-ui-calendar-widget-before` | `vendor/ibexa/calendar/src/bundle/Resources/views/themes/admin/calendar/view.html.twig` | ## Site Context | Group name | Template file | | ------------------------------ | ------------------------------------------------------------------------------------------------ | | `admin-ui-content-tree-before` | `vendor/ibexa/site-context/src/bundle/Resources/views/themes/admin/content/fullscreen.html.twig` | | `admin-ui-content-tree-after` | `vendor/ibexa/site-context/src/bundle/Resources/views/themes/admin/content/fullscreen.html.twig` | ## Product Catalog | Group name | Template file | | --------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------- | | `admin-ui-attribute-definition-block` | `vendor/ibexa/product-catalog/src/bundle/Resources/views/themes/admin/product_catalog/attribute_definition/view.html.twig` | | `admin-ui-attribute-definition-options-block` | `pvendor/ibexa/roduct-catalog/src/bundle/Resources/views/themes/admin/product_catalog/attribute_definition/tab/details.html.twig` | | `admin-ui-attribute-group-block` | `vendor/ibexa/product-catalog/src/bundle/Resources/views/themes/admin/product_catalog/attribute_group/view.html.twig` | | `admin-ui-catalog-block` | `vendor/ibexa/product-catalog/src/bundle/Resources/views/themes/admin/product_catalog/catalog/view.html.twig` | | `admin-ui-customer-group-block` | `vendor/ibexa/product-catalog/src/bundle/Resources/views/themes/admin/product_catalog/customer_group/view.html.twig` | | `admin-ui-form-product-add-translation-body` | `vendor/ibexa/product-catalog/src/bundle/Resources/views/themes/admin/product_catalog/product/modal/add_translation.html.twig` | | `admin-ui-product-create-form-header-actions` | `vendor/ibexa/product-catalog/src/bundle/Resources/views/themes/admin/product_catalog/product/create.html.twig` | | `admin-ui-product-create-form-after` | `vendor/ibexa/product-catalog/src/bundle/Resources/views/themes/admin/product_catalog/product/create.html.twig` | | `admin-ui-product-edit-form-header-actions` | `vendor/ibexa/product-catalog/src/bundle/Resources/views/themes/admin/product_catalog/product/edit.html.twig` | | `admin-ui-product-edit-form-after` | `vendor/ibexa/product-catalog/src/bundle/Resources/views/themes/admin/product_catalog/product/edit.html.twig` | | `admin-ui-product-block` | `vendor/ibexa/product-catalog/src/bundle/Resources/views/themes/admin/product_catalog/product/view.html.twig` | | `admin-ui-product-translation-modal-footer` | `vendor/ibexa/product-catalog/src/bundle/Resources/views/themes/admin/product_catalog/product/modal/add_translation.html.twig` | | `admin-ui-product-translations-actions-modal` | `vendor/ibexa/product-catalog/src/bundle/Resources/views/themes/admin/product_catalog/product/tab/translations.html.twig` | | `admin-ui-product-translations-actions` | `vendor/ibexa/product-catalog/src/bundle/Resources/views/themes/admin/product_catalog/product/tab/translations.html.twig` | | `admin-ui-product-translations-row-actions` | `vendor/ibexa/product-catalog/src/bundle/Resources/views/themes/admin/product_catalog/product/tab/translations.html.twig` | | `admin-ui-product-type-block` | `vendor/ibexa/product-catalog/src/bundle/Resources/views/themes/admin/product_catalog/product_type/view.html.twig` | ## Taxonomy | Group name | Template file | | ----------------------------------- | ------------------------------------------------------------------------------------------------------------ | | `admin-ui-location-view-tab-groups` | `vendor/ibexa/taxonomy/src/bundle/Resources/views/themes/admin/ibexa/taxonomy/taxonomy_entry/show.html.twig` | ## Page Builder (Experience) | Group name | Template file | | --------------------------------- | ------------------------------------------------------------------------------------------ | | `admin-ui-infobar-options-before` | `vendor/ibexa/page-builder/src/bundle/Resources/views/page_builder/infobar/base.html.twig` | ## AI Actions | Group name | Template file | | ------------------------------------ | -------------------------------------------------------------------------------------------------------------------- | | `admin-ui-action-configuration-tabs` | `vendor/ibexa/connector-ai/src/bundle/Resources/views/themes/admin/connector_ai/action_configuration/view.html.twig` | # Formatting date and time > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). Use different formats to render dates and times in the back office and website front. Two methods exist that allow you to specify how the date and time should be formatted. ## With Twig filters and PHP services You can format date and time by using the following services: - `@ibexa.user.settings.short_datetime_format.formatter` - `@ibexa.user.settings.short_datet_format.formatter` - `@ibexa.user.settings.short_time_format.formatter` - `@ibexa.user.settings.full_datetime_format.formatter` - `@ibexa.user.settings.full_date_format.formatter` - `@ibexa.user.settings.full_time_format.formatter` To use them, create an `src/Service/MyService.php` file containing: ```php shortDateTimeFormatter->format($now); $utc = $this->shortDateTimeFormatter->format($now, 'UTC'); // your code } } ``` Then, add the following to `config/services.yaml`: ```yaml services: App\Service\MyService: arguments: $shortDateTimeFormatter: '@ibexa.user.settings.short_datetime_format.formatter' ``` ## Within User settings menu Users can set their preferred date and time formats in the user settings menu. This format is used throughout the back office. You can set the list of available formats under the `ibexa.system..user_preferences` [configuration key](https://doc.ibexa.co/en/saas/administration/configuration/configuration/#configuration-files): ```yaml ibexa: system: : user_preferences: allowed_short_date_formats: 'label for dd/MM/yyyy': 'dd/MM/yyyy' 'label for MM/dd/yyyy': 'MM/dd/yyyy' allowed_short_time_formats: 'label for HH:mm' : 'HH:mm' 'label for hh:mm a' : 'hh:mm a' allowed_full_date_formats: 'label for dd/MM/yyyy': 'dd/MM/yyyy' 'label for MM/dd/yyyy': 'MM/dd/yyyy' allowed_full_time_formats: 'label for HH:mm': 'HH:mm' 'label for hh:mm a': 'hh:mm a' ``` The default date and time format is set using: ```yaml ibexa: system: : user_preferences: short_datetime_format: date_format: 'dd/MM/yyyy' time_format: 'hh:mm' full_datetime_format: date_format: 'dd/MM/yyyy' time_format: 'hh:mm' ``` ## Allowed formats The following subset of the [ICU date and time formats](https://unicode-org.github.io/icu-docs/apidoc/released/icu4c/classSimpleDateFormat.html#details) is allowed: | Symbol | Meaning | | ---------------------------------------------------------------------------- | ---------------- | | y, yy, yyyy, Y, YY, YYYY | year | | q, Q | quarter | | M, MM, MMM, MMMM, L, LL, LLL, LLLL | month | | w, WW | week | | d, dd | day of the month | | D, DDD | day of the year | | E, EE, EEE, EEEE, EEEEEE, e, ee, eee, eeee, eeeeee, c, cc, ccc, cccc, cccccc | weekday | | a | AM or PM | | h, hh, H, HH, k, kk | hour | | m, mm | minute | | s, ss, S... | second | | Z, ZZ, ZZZ, ZZZZZ | timezone | # Extending thumbnails > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). Customize thumbnails use for content items in the back office. The thumbnails API enable you to choose an image for a specific content. If you don't want to use custom thumbnails, `ContentType` is used instead. ## Thumbnail mechanism The thumbnail mechanism has two layers, and each layer can have many implementations. The mechanism checks if any of the implementations returns a field, for example, `ibexa_image`, that has function "Can be a thumbnail" turned on. ![Can be a thumbnail setting](https://doc.ibexa.co/en/saas/administration/img/extending_thumbnail_can_be.png) If found, the image is used as a content type thumbnail. ### First layer First layer of the mechanism contains strategy pattern that focuses on finding a thumbnail source. The thumbnail can be found inside or outside the content type. For example for users thumbnails can be downloaded from an avatar-generating service. For this layer there are following default implementations: - The mechanism looks for fields that can be thumbnail, if found, the mechanism moves to the second layer. - If there are no fields that can be a thumbnail, the content type icon is used as a thumbnail. ### Second layer Second layer of mechanism enables selection of thumbnail from a field that the first layer has found. It searches the content type for all the fields, for example, images, with function "Can be a thumbnail" turned on. If there is more than one field in the content type that can be used as a thumbnail, this layer returns the first nonempty field as a thumbnail. This mechanism can be modified to fit your site needs, so you can decide from where and how the thumbnails is downloaded. ### Create a thumbnail mechanism First, create base strategy for returning custom thumbnails from a static file. Create `StaticStrategy.php` in `src/Strategy`. ```php $this->staticThumbnail, ]); } } ``` Next, add the strategy with the `ibexa.repository.thumbnail.strategy.content` tag and `priority: 100` to `config/services.yaml`: ```yaml services: App\Thumbnails\FieldValueUrl: tags: - { name: ibexa.repository.thumbnail.strategy.field, priority: 100 } ``` Priority `100` allows this strategy to be used first on a clean installation or before any other strategy with lower priority. At this point you can go to the back office and check the results. > **Note: Thumbnail mechanism** > > This strategy overrides all generated thumbnails. You can specify a specific content type. See the example [here](https://github.com/ibexa/user/blob/6.0/src/lib/Strategy/DefaultThumbnailStrategy.php) ## Other fields as thumbnails Any field type can generate a thumbnail, for example: - DateAndTime (`ibexa_datetime`) - you can add a mini calendar thumbnail for Appointment content type and on the day of the appointment a clock thumbnail with a specific time when it takes place - TextBlock (`ibexa_text`) - you can add a first letter of the text block that is inside ### Add ibexa_text field as thumbnail First, create a strategy that adds support for `ibexa_text` as the thumbnail. It enables you to add a thumbnail URL in the text field. Add `FieldValueUrl.php` in `src/Thumbnails`. ```php $field->value, ]); } } ``` Next, add the strategy with the `ibexa.repository.thumbnail.strategy.field` tag to `config/services.yaml`: ```yaml App\Thumbnails\FieldValueUrl: tags: - { name: ibexa.repository.thumbnail.strategy.field, priority: 100 } ``` At this point you can go to the back office and check the results. # Importing assets from a bundle > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). Import assets, such as stylesheets or images, from a separate bundle with customizations. Cohesivo uses [Webpack Encore](https://symfony.com/doc/7.4/frontend.html#webpack-encore) for asset management. ## Configuration from a bundle To import assets from a bundle, configure them in an `ibexa.config.js` file that you create either in the bundle's `Resources/encore/` folder, or in the `encore` folder in the root directory of your project: ```js const path = require('path'); module.exports = (Encore) => { Encore.addEntry('', [ path.resolve(__dirname, ''), ]); }; ``` Use `` to refer to this configuration entry from Twig templates: `{{ encore_entry_script_tags('', null, 'ibexa') }}` To import CSS files only, use: `{{ encore_entry_link_tags('', null, 'ibexa') }}` > **Tip: Tip** > > After you add new files, run `php bin/console cache:clear`. > > For a full example of importing asset configuration, see [`ibexa.config.js`](https://github.com/ibexa/admin-ui/blob/6.0/src/bundle/Resources/encore/ibexa.config.js) To edit existing configuration entries, either in the bundle's `Resources/encore/` folder, or in the `encore` folder in the root folder of your project, create an `ibexa.config.manager.js` file: ```js const path = require('path'); module.exports = (ibexaConfig, ibexaConfigManager) => { ibexaConfigManager.replace({ ibexaConfig, entryName: '', itemToReplace: path.resolve(__dirname, ''), newItem: path.resolve(__dirname, ''), }); ibexaConfigManager.remove({ ibexaConfig, entryName: '', itemsToRemove: [ path.resolve(__dirname, ''), path.resolve(__dirname, ''), ], }); ibexaConfigManager.add({ ibexaConfig, entryName: '', newItems: [ path.resolve(__dirname, ''), path.resolve(__dirname, ''), ], }); }; ``` > **Tip: Tip** > > If you don't know what `entryName` to use, you can use the browser's developer tools to check what files are loaded on the given page. Then, use the file name as `entryName`. > **Tip: Tip** > > After you add new files, run `php bin/console cache:clear`. > > For a full example of overriding configuration, see [`ibexa.config.manager.js`](https://github.com/ibexa/fieldtype-matrix/blob/6.0/src/bundle/Resources/encore/ibexa.config.manager.js). To add a new configuration under your own namespace and with its own dependencies, create an `ibexa.webpack.custom.config.js` file that you create either in the bundle's `Resources/encore/` folder, or in the `encore` folder in the root directory of your project, for example: ```js const Encore = require('@symfony/webpack-encore'); Encore.setOutputPath('') .setPublicPath('') .addExternals('') // ... .addEntry('', ['']); const customConfig = Encore.getWebpackConfig(); customConfig.name = 'customConfigName'; // Config or array of configs: [customConfig1, customConfig2]; module.exports = customConfig; ``` > **Tip: Tip** > > If you don't plan to add multiple entry files on the same page in your custom configuration, use the `disableSingleRuntimeChunk()` function to avoid adding a separate `runtime.js` file. Otherwise, your JS code may be run multiple times. By default, the `enableSingleRuntimeChunk()` function is used. ## Configuration from main project files If you prefer to include the asset configuration in the main project files, add it in [`webpack.config.js`](https://github.com/ibexa/recipes/blob/master/ibexa/oss/4.6/encore/webpack.config.js#L26). To overwrite the built-in assets, use the following configuration to replace, remove, or add asset files in `webpack.config.js`: ```js const ibexaConfigManager = require('./ibexa.webpack.config.manager.js'); //... ibexaConfigManager.replace({ ibexaConfig, entryName: '', itemToReplace: path.resolve(__dirname, ''), newItem: path.resolve(__dirname, ''), }); ibexaConfigManager.remove({ ibexaConfig, entryName: '', itemsToRemove: [ path.resolve(__dirname, ''), path.resolve(__dirname, ''), ], }); ibexaConfigManager.add({ ibexaConfig, entryName: '', newItems: [ path.resolve(__dirname, ''), path.resolve(__dirname, ''), ], }); ``` # Back office tabs > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). Tabs are used for content view, in dashboard, system information and other parts of the back office and are extensible. Many elements of the back office interface, such as content view, dashboard, or system information, are built with tabs. ![Tabs in System Information](https://doc.ibexa.co/en/saas/administration/img/tabs_system_info.png) You can extend existing tab groups with new tabs, or create your own tab groups. ## Tabs A custom tab can extend one of the following classes: - [`AbstractTab`](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-AdminUi-Tab-AbstractTab.html) - base tab - [`AbstractControllerBasedTab`](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-AdminUi-Tab-AbstractControllerBasedTab.html) - embeds the results of a controller action in the tab - [`AbstractRouteBasedTab`](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-AdminUi-Tab-AbstractRouteBasedTab.html) - embeds the results of the selected route, passing applicable parameters ```php //... class EveryoneArticleTab extends AbstractTab implements OrderedTabInterface { //... public function getIdentifier(): string { return 'everyone-article'; } public function renderView(array $parameters): string { //... return $this->twig->render('@ibexadesign/ui/dashboard/tab/all_content.html.twig', [ 'data' => $this->pagerLocationToDataMapper->map($pager, true), ]); } } ``` > **Tip: Tip** > > For a full example of creating a custom tab, see [Add dashboard tab](https://doc.ibexa.co/en/saas/administration/back_office/back_office_tabs/create_dashboard_tab/index.md). You need to register the tab as a service. Tag it with `ibexa.admin_ui.tab` and indicate the group in which it should appear: ```yaml services: App\Tab\Dashboard\Everyone\EveryoneArticleTab: autowire: true autoconfigure: true public: false tags: - { name: ibexa.admin_ui.tab, group: dashboard-everyone } ``` The group can be one of the existing components, or your own [custom tab group](#tab-groups). ### Tab order You can order the tabs by making the tab implement `OrderedTabInterface`. The order depends on the numerical value returned by the `getOrder` method: ```php public function getOrder(): int { return 300; } ``` Tabs are displayed according to this value in ascending order. > **Tip: Tip** > > It's a good practice to reserve some distance between these values, for example to stagger them by step of 10. It may come useful if you later need to place something between the existing tabs. You can also influence tab display (for example, order tabs, remove, or modify them) by using the following event listeners: - `TabEvents::TAB_GROUP_PRE_RENDER` - `TabEvents::TAB_PRE_RENDER` ## Tab groups You can create new tab groups by using the [`TabsComponent`](https://github.com/ibexa/admin-ui/blob/6.0/src/lib/Component/TabsComponent.php). To create a tab group, register it as a service: ```yaml services: app.my_tabs.custom_group: parent: Ibexa\AdminUi\Component\TabsComponent arguments: $groupIdentifier: 'custom_group' tags: - { name: ibexa.twig.component, group: 'admin-ui-dashboard-blocks' } ``` Tag the group with `ibexa.twig.component`. `group` indicates where the group is rendered. To learn more about this mechanism, see [Twig Components](https://doc.ibexa.co/en/saas/templating/components/index.md). And for the groups available in the back office, see [custom components in the back office](https://doc.ibexa.co/en/saas/administration/back_office/back_office_elements/custom_components/index.md). # Create dashboard tab > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). Add a new tab to the back office dashboard that welcomes every user after logging in. To create a new tab in the dashboard, create an `EveryoneArticleTab.php` file in `src/Tab/Dashboard/Everyone`. This adds a tab to the **Common content** dashboard block that displays all articles in the repository. ```php sortClauses = [new SortClause\DateModified(LocationQuery::SORT_DESC)]; $query->query = new Criterion\LogicalAnd([ new Criterion\ContentTypeIdentifier('article'), ]); $pager = new Pagerfanta( new LocationSearchAdapter( $query, $this->searchService ) ); $pager->setMaxPerPage($limit); $pager->setCurrentPage($page); return $this->twig->render('@ibexadesign/ui/dashboard/tab/all_content.html.twig', [ 'data' => $this->pagerLocationToDataMapper->map($pager, true), ]); } } ``` This tab searches for content with content type "Article" (lines 50-53) and uses the built-in `all_content.html.twig` template to render the results, which ensures that the tab looks the same as the existing tabs (lines 64-66). The tab also implements [`OrderedTabInterface`](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-AdminUi-Tab-OrderedTabInterface.html) (line 17), which enables you to define the order in which the tab is displayed on the dashboard page. It's done with the `getOrder()` method (line 38). Register this tab as a service: ```yaml services: App\Tab\Dashboard\Everyone\EveryoneArticleTab: autowire: true autoconfigure: true public: false tags: - { name: ibexa.admin_ui.tab, group: dashboard-everyone } ``` # Tab switcher in content edit page > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). Tabs switcher allows separating the default field types in the content type from the field types that enhance the content with new functionalities. Tabs switcher allows separating the default field types in the content type from the field types that enhance the content with new functionalities. The best example of such field types are SEO or Taxonomy, as these aren't typical field types but a field types that handle functionalities for the whole content object. The following example shows how to add a Meta tab with automatically assigned Taxonomy field type. ## Add Meta tab Before you start adding the Meta tab, make sure the content type you want to edit has [Taxonomy Entry Assignment field type](https://doc.ibexa.co/projects/userguide/en/6.0/content_management/taxonomy/work_with_tags/#add-taxonomy-entry-assignment-field-to-content-type). Next, provide the semantic configuration under the `ibexa.system..admin_ui_forms` [configuration key](https://doc.ibexa.co/en/saas/administration/configuration/configuration/#configuration-files): ```yaml ibexa: system: admin_group: admin_ui_forms: content_edit: fieldtypes: ibexa_taxonomy_entry_assignment: meta: true ``` `ibexa_taxonomy_entry_assignment` - identifier for the field type `meta` - when set to `true`, puts the declared field type in the Meta tab ![Meta tab](https://doc.ibexa.co/en/saas/administration/img/tab_switcher.png) ### Configure field groups for Meta tab The default configuration makes the `ibexa_taxonomy_entry_assignment` field always visible in the Meta tab in the content form. With this new feature, you can indicate what field types, previously set in the back office content type, are shown in the Meta tab section in the content form. You can automatically move all field types from Metadata group to the Meta tab in the content form. To do it, use the following configuration: ```yaml ibexa: system: admin_group: admin_ui_forms: content_edit: meta_field_groups_list: - metadata ``` ![Meta tab](https://doc.ibexa.co/en/saas/administration/img/tab_switcher_meta.png) To disable the feature: ```yaml ibexa: system: admin_group: admin_ui_forms: content_edit: meta_field_groups_list: [] ``` The `meta_field_groups_list` configuration can be overridden. ## Add custom tab First, create an event listener in the `src/EventListener/TextAnchorMenuTabListener.php`: ```php 'onAnchorMenuConfigure']; } public function onAnchorMenuConfigure(ConfigureMenuEvent $event): void { // access anchor menu root item $menu = $event->getMenu(); // if you need to access "Content" tab, use ITEM__CONTENT constant: $contentTab = $menu[ContentEditAnchorMenuBuilder::ITEM__CONTENT]; // if you need to access "Meta" tab, use ITEM__META constant: $metaTab = $menu[ContentEditAnchorMenuBuilder::ITEM__META]; // Adding new tab called "New tab" $menu->addChild('New tab', ['attributes' => ['data-target-id' => 'ibexa-edit-content-sections-new-tab']]); // Add second level item "2nd level item" to previously created "New tab" tab $menu['New tab']->addChild('2nd level item', ['attributes' => ['data-target-id' => 'ibexa-edit-content-sections-new-tab-item_2']]); } } ``` A new custom tab is defined in the line 28, the line 31 defines items for the second level. For new tabs it's also required to render its section in the content editing form. To do it, register the UI Component: ```yaml services: app.custom_content_edit_tab: parent: Ibexa\AdminUi\Component\TwigComponent arguments: $template: '@@ibexadesign/content_type/edit/custom_tab.html.twig' tags: - { name: ibexa.twig.component, group: 'admin-ui-content-edit-sections' } ``` Finally, create the `templates/themes/admin/content_type/edit/custom_tab.html.twig` file: ```html+twig {% extends '@ibexadesign/ui/component/anchor_navigation/section_group.html.twig' %} {% set data_id = 'ibexa-edit-content-sections-new-tab' %} {% block sections %} {% embed '@ibexadesign/ui/component/anchor_navigation/section.html.twig' %} {% set data_id = 'ibexa-edit-content-sections-new-tab-item_2' %} {% block content %} Contents of custom secondary section {% endblock %} {% endembed %} {% endblock %} ``` # Add anchor menu to content type edit screen > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). Add anchor menu to the content type configuration screen, to make field type settings of your choice more prominent. With the anchor menu you can increase visibility of certain [field types](https://doc.ibexa.co/en/saas/content_management/field_types/field_types/index.md), which provide more complex functionality, by separating them from the [field definitions](https://doc.ibexa.co/en/saas/administration/content_organization/content_types/#field-definitions) section in [content type](https://doc.ibexa.co/en/saas/administration/content_organization/content_types/index.md) configuration screen. One example of such field type would be [SEO](https://doc.ibexa.co/projects/userguide/en/6.0/search_engine_optimization/work_with_seo/), because it handles functionality that applies to all content items of the content type. You can use the anchor menu feature with other field types, including [custom ones](https://doc.ibexa.co/en/saas/content_management/field_types/create_custom_generic_field_type/index.md). See the following example to learn how you can add a field type as an anchor menu. ## Modify YAML configuration Modify the field type visibility under the `ibexa.system..admin_ui_forms.content_type_edit.field_types` [configuration key](https://doc.ibexa.co/en/saas/administration/configuration/configuration/#configuration-files): ```yaml ibexa: system: admin_group: admin_ui_forms: content_type_edit: field_types: : meta: true position: 100 ``` Where keys have the following meaning: - `field_type_identifier` - replace this key with an identifier of the field type that you want to make more prominent. In case of SEO, this key is `ibexa_seo`. - `meta` - when this flag is set to `true`, it separates the field type from the **Field definitions** section and puts it in an anchor menu - `position` - decides about the field type's position on the content type edit screen and in the content item, in relation to other field types Additionally, setting `meta` to `true` adds a toggle for enabling or disabling the field type. In case of SEO, it adds the **Enable SEO for this content type** toggle. Enable the toggle to display the SEO section on the content item edit page. ![SEO anchor menu](https://doc.ibexa.co/en/saas/administration/img/content_type_edit_screen_anchor_menu.png) > **Note: Note** > > If you add multiple field types as anchor menus, they're automatically displayed as separate sections. # Back office menus > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). All menus in the back office are based on KnpMenuBundle and you can easily extend them with new items. Back office menus are based on the [KnpMenuBundle](https://github.com/KnpLabs/KnpMenuBundle) and they're extensible. > **Tip: Tip** > > For general information on how to use `MenuBuilder`, see [the official KnpMenuBundle documentation](https://symfony.com/bundles/KnpMenuBundle/current/index.html). Menus are extensible using event subscribers, for example: ```php ['onMainMenuConfigure', 0], ]; } public function onMainMenuConfigure(ConfigureMenuEvent $event): void { $menu = $event->getMenu(); $customMenuItem = $menu[MainMenuBuilder::ITEM_CONTENT]->addChild( 'main__content__custom_menu', [ 'extras' => [ 'orderNumber' => 100, ], ], ); } } ``` > **Tip: Tip** > > The event subscriber is registered as a service by default, if `autoconfigure` is enabled. If not, register it as a service and tag with `kernel.event.subscriber`. ## Menu events You can listen to the following events: | | | | ------------------------ | ------------------------------------------------------------- | | Main menu | `ConfigureMenuEvent::MAIN_MENU` | | | `ConfigureMenuEvent::USER_MENU` | | Content view | `ConfigureMenuEvent::CONTENT_SIDEBAR_RIGHT` | | | `ConfigureMenuEvent::CONTENT_EDIT_SIDEBAR_RIGHT` | | | `ConfigureMenuEvent::CONTENT_CREATE_SIDEBAR_RIGHT` | | | `ConfigureMenuEvent::CONTENT_SIDEBAR_LEFT` | | Trash | `ConfigureMenuEvent::TRASH_SIDEBAR_RIGHT` | | Section | `ConfigureMenuEvent::SECTION_EDIT_SIDEBAR_RIGHT` | | | `ConfigureMenuEvent::SECTION_CREATE_SIDEBAR_RIGHT` | | Policies and permissions | `ConfigureMenuEvent::POLICY_EDIT_SIDEBAR_RIGHT` | | | `ConfigureMenuEvent::POLICY_CREATE_SIDEBAR_RIGHT` | | | `ConfigureMenuEvent::ROLE_EDIT_SIDEBAR_RIGHT` | | | `ConfigureMenuEvent::ROLE_CREATE_SIDEBAR_RIGHT` | | | `ConfigureMenuEvent::ROLE_COPY_SIDEBAR_RIGHT` | | | `ConfigureMenuEvent::USER_EDIT_SIDEBAR_RIGHT` | | | `ConfigureMenuEvent::USER_CREATE_SIDEBAR_RIGHT` | | | `ConfigureMenuEvent::ROLE_ASSIGNMENT_CREATE_SIDEBAR_RIGHT` | | Languages | `ConfigureMenuEvent::LANGUAGE_CREATE_SIDEBAR_RIGHT` | | | `ConfigureMenuEvent::LANGUAGE_EDIT_SIDEBAR_RIGHT` | | Object states | `ConfigureMenuEvent::OBJECT_STATE_GROUP_CREATE_SIDEBAR_RIGHT` | | | `ConfigureMenuEvent::OBJECT_STATE_GROUP_EDIT_SIDEBAR_RIGHT` | | | `ConfigureMenuEvent::OBJECT_STATE_CREATE_SIDEBAR_RIGHT` | | | `ConfigureMenuEvent::OBJECT_STATE_EDIT_SIDEBAR_RIGHT` | | Content types | `ConfigureMenuEvent::CONTENT_TYPE_GROUP_CREATE_SIDEBAR_RIGHT` | | | `ConfigureMenuEvent::CONTENT_TYPE_GROUP_EDIT_SIDEBAR_RIGHT` | | | `ConfigureMenuEvent::CONTENT_TYPE_CREATE_SIDEBAR_RIGHT` | | | `ConfigureMenuEvent::CONTENT_TYPE_EDIT_SIDEBAR_RIGHT` | | | `ConfigureMenuEvent::CONTENT_TYPE_SIDEBAR_RIGHT` | | URLs and wildcards | `ConfigureMenuEvent::URL_EDIT_SIDEBAR_RIGHT` | | | `ConfigureMenuEvent::URL_WILDCARD_EDIT_SIDEBAR_RIGHT` | | User settings | `ConfigureMenuEvent::USER_PASSWORD_CHANGE_SIDEBAR_RIGHT` | | | `ConfigureMenuEvent::USER_SETTING_UPDATE_SIDEBAR_RIGHT` | ## Adding menu items To add a menu item, use the `addChild()` method. Provide the method with the new menu item's identifier and, optionally, with parameters. To add an inactive menu section, don't add a route to its parameters. The following method adds a new menu section under **Content**, and under it, a new item with custom attributes: ```php $customMenuItem->addChild( 'all_content_list', [ 'label' => 'Content List', 'route' => 'all_content_list.list', 'attributes' => [ 'class' => 'custom-menu-item', ], 'linkAttributes' => [ 'class' => 'custom-menu-item-link', ], ] ); ``` `label` is used for the new menu item in the interface. `route` is the name of the route that the menu item leads to. `attributes` adds attributes (such as CSS classes) to the container `
  • ` element of the new menu item. `linkAttributes` adds attributes to the `` element. ### Passing a parameter to a menu item You can also pass parameters to templates used to render menu items with `template_parameters`: ```php /** @var \Knp\Menu\ItemInterface $menu */ $menu->addChild( 'all_content_list', [ 'extras' => [ 'template' => '@ibexadesign/list/all_content_list.html.twig', 'template_parameters' => [ 'custom_parameter' => 'value', ], ], ] ); ``` You can then use the variable `custom_parameter` in `templates/themes/admin/list/all_content_list.html.twig`. ### Translatable labels To have translatable labels, use `translation.key` from the `messages` domain: ```php /** @var \Knp\Menu\ItemInterface $menu */ $menu->addChild( 'all_content_list', [ 'label' => 'translation.key', 'extras' => [ 'translation_domain' => 'messages', ], ] ); ``` ## Modifying menu items To modify the parameters of an existing menu item, use the `setExtra()` method. ### Custom icons You can use the `extras.icon` parameter to define an icon for a menu item. For example, the following code changes the default icon for the **Create content** button in content view: ```php $menu->getChild('main__admin') ->setExtra('icon_path', '/bundles/ibexaadminuiassets/vendors/ids-assets/dist/img/all-icons.svg#alert-error'); ``` ## Removing menu items To remove a menu item, for example, to remove the **Copy subtree** item from the right menu in content view, use the following event listener: ```php $menu->removeChild('main__bookmarks'); ``` # Add menu item > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). Create a custom menu in the back office. To add a new menu entry in the back office, you need to use an event subscriber and subscribe to [one of the events](https://doc.ibexa.co/en/saas/administration/back_office/back_office_menus/back_office_menus/#menu-events) dispatched when building menus. The following example shows how to add a "Content list" item to the main top menu and list all content items there, with a shortcut button to edit them. ## Create event subscriber First, create an event subscriber in `src/EventSubscriber/MyMenuSubscriber.php`: ```php ['onMainMenuConfigure', 0], ]; } public function onMainMenuConfigure(ConfigureMenuEvent $event): void { $menu = $event->getMenu(); $customMenuItem = $menu[MainMenuBuilder::ITEM_CONTENT]->addChild( 'main__content__custom_menu', [ 'extras' => [ 'orderNumber' => 100, ], ], ); $customMenuItem->addChild( 'all_content_list', [ 'label' => 'Content List', 'route' => 'all_content_list.list', 'attributes' => [ 'class' => 'custom-menu-item', ], 'linkAttributes' => [ 'class' => 'custom-menu-item-link', ], ] ); } } ``` This subscriber subscribes to the `ConfigureMenuEvent::MAIN_MENU` event (see line 14). It creates a subitem with the identifier `main__content__custom_menu` (lines 22-23). Then, under this subitem, it creates an `all_content_list` menu item (lines 31-32). ## Add route Next, configure the route that the menu item leads to: ```yaml all_content_list.list: path: /all_content_list/{page} defaults: page: 1 _controller: App\Controller\AllContentListController::listAction ``` ## Create controller The route indicates a controller that fetches all visible content items and renders the view. Create the following controller file in `src/Controller/AllContentListController.php`: ```php query = new Criterion\Visibility(Criterion\Visibility::VISIBLE); $paginator = new Pagerfanta( new LocationSearchAdapter($query, $this->searchService) ); $paginator->setMaxPerPage(8); $paginator->setCurrentPage($page); $editForm = $this->formFactory->contentEdit(); return $this->render('@ibexadesign/all_content_list.html.twig', [ 'totalCount' => $paginator->getNbResults(), 'articles' => $paginator, 'form_edit' => $editForm, ]); } } ``` ## Add template Finally, create the `templates/themes/admin/list/all_content_list.html.twig` file indicated in line 37 in the controller: ```html+twig {% extends '@ibexadesign/ui/layout.html.twig' %} {% block title %}{{ 'Content List'|trans }}{% endblock %} {%- block breadcrumbs -%} {% include '@ibexadesign/ui/breadcrumbs.html.twig' with { items: [ { value: 'breadcrumb.admin'|trans(domain='messages')|desc('Admin') }, { value: 'url.list'|trans|desc('Content List') } ]} %} {%- endblock -%} {%- block header -%} {% include '@ibexadesign/ui/page_title.html.twig' with { title: 'url.list'|trans|desc('Content List'), } %} {%- endblock -%} {%- block content -%}
    {% set body_rows = [] %} {% for article in articles.currentPageResults %} {% set col_edit %} {% endset %} {% set body_rows = body_rows|merge([{ cols: [ { content: article.contentInfo.name }, { content: article.contentInfo.contentType.name }, { content: article.contentInfo.modificationDate|ibexa_full_datetime }, { content: article.contentInfo.publishedDate|ibexa_full_datetime }, { content: col_edit, raw: true }, ], }]) %} {% endfor %} {% include '@ibexadesign/ui/component/table/table.html.twig' with { headline: 'Content List', head_cols: [ { content: 'Content name'|trans }, { content: 'Content type'|trans }, { content: 'Modified'|trans }, { content: 'Published'|trans }, { content: '' }, ], class: 'ibexa-table', body_rows } %} {% if articles.haveToPaginate %} {% include '@ibexadesign/ui/pagination.html.twig' with { 'pager': articles } %} {% endif %}
    {{ form_start(form_edit, { 'action': path('ibexa.content.edit'), 'attr': { 'class': 'ibexa-edit-content-form'} }) }} {{ form_widget(form_edit.language, {'attr': {'hidden': 'hidden', 'class': 'language-input'}}) }} {{ form_end(form_edit) }} {% include '@ibexadesign/content/modal/version_conflict.html.twig' %} {%- endblock -%} {% block javascripts %} {{ encore_entry_script_tags('ibexa-admin-ui-dashboard-js', null, 'ibexa') }} {%- endblock -%} ``` This template uses the [reusable table template](https://doc.ibexa.co/en/saas/administration/back_office/back_office_elements/reusable_components/#tables) to render a table that fits the style of the back office. You can configure the columns of the table in the `head_cols` variable and the regular table rows in `body_rows`. In this case, `body_rows` contains information about the content item provided by the controller, and an edit button. # Add user setting > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). Add the option to select a custom preference in user menu. ## Create new user setting You can add new preferences to the **User Settings** menu in the back office. To do so, create a setting class implementing two interfaces: `ValueDefinitionInterface` and `FormMapperInterface`. In this example the class is located in `src/Setting/Unit.php` and enables the user to select their preference for metric or imperial unit systems. ```php 'Metric', self::IMPERIAL_OPTION => 'Imperial', default => throw new InvalidArgumentException( '$storageValue', sprintf('There is no \'%s\' option', $storageValue) ), }; } public function getDefaultValue(): string { return 'metric'; } public function mapFieldForm(FormBuilderInterface $formBuilder, ValueDefinitionInterface $value): FormBuilderInterface { $choices = [ 'Metric' => self::METRIC_OPTION, 'Imperial' => self::IMPERIAL_OPTION, ]; return $formBuilder->create( 'value', ChoiceType::class, [ 'multiple' => false, 'required' => true, 'label' => $this->getDescription(), 'choices' => $choices, ] ); } } ``` Register the setting as a service: ```yaml services: App\Setting\Unit: tags: - { name: ibexa.user.setting.value, identifier: unit, group: my_group, priority: 50 } - { name: ibexa.user.setting.mapper.form, identifier: unit } ``` You can order the settings in the **User** menu by setting their `priority`. `group` indicates the group that the setting is placed in. It can be one of the built-in groups, or a custom one. To create a custom setting group, create an `App/Setting/Group/MyGroup.php` file: ```php .user_settings_update_view` [configuration key](https://doc.ibexa.co/en/saas/administration/configuration/configuration/#configuration-files): ```yaml ibexa: system: admin_group: user_settings_update_view: full: unit: template: '@ibexadesign/user/setting/update_unit.html.twig' match: Identifier: [ unit ] ``` The `templates/themes/admin/user/setting/update_unit.html.twig` template must extend the `@ibexadesign/account/settings/update.html.twig` template: ```html+twig {% extends '@ibexadesign/account/settings/update.html.twig' %} {% block form %} {{ parent() }} {% endblock %} ``` # Customize calendar > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). Add custom events to the calendar and customize its looks. By default, the Calendar displays scheduled events of the following types: - Content publication (`future_publication`) - Content hide (`future_hide`) - Block reveal (`page_block_reveal`) - Block hide (`page_block_hide`) You can perform basic actions on these events. You can also configure the calendar to display custom event types. ## Customize colors and icons You can change the color of a calendar event or change the icon of an action. The setting is SiteAccess-aware. To customize the appearance settings, use the `ibexa.system..calendar.event_types` [configuration key](https://doc.ibexa.co/en/saas/administration/configuration/configuration/#configuration-files): ```yaml ibexa: system: admin_group: calendar: event_types: future_publication: color: '#47BEDB' actions: reschedule: icon: '/bundles/ibexaadminuiassets/vendors/ids-assets/dist/img/all-icons.svg#alert-error' ``` Line 6 contains the name of the event type, either a built-in custom one. `color` defines the color in which events of this type are displayed in the calendar. `icon` is the icon used for a button with the relevant event action. ![Bank holiday with custom color](https://doc.ibexa.co/en/saas/administration/img/extending_calendar_view.png) ## Configure custom events The following example shows how to create custom events which add different holidays to the calendar. First, create a new event in `src/Calendar/Holidays/Event.php`: ```php actions = new EventActionCollection($actions); } public function getTypeIdentifier(): string { return self::EVENT_TYPE_IDENTIFIER; } public function getTypeLabel(): string { return 'Holidays'; } public function getEventName(Event $event): string { return $event->getId(); } public function getActions(): EventActionCollection { return $this->actions; } } ``` You can use the identifier defined in lines 20-23 to configure [event colors](#customize-colors-and-icons). Complete the procedure by registering the new event type as a service: ```yaml services: App\Calendar\Holidays\EventType: arguments: $actions: [ ] tags: - { name: ibexa.calendar.event.type } ``` ## Configure event sources To add specific events to your calendar, you need to create an event source. An event source must implement `Ibexa\Contracts\Calendar\EventSource\EventSourceInterface`. One such built-in implementation is `InMemoryEventSource`. To add an in-memory collection as an event source, create `src/Calendar/Holidays/EventSourceFactory.php`: ```php createEvent('April Fools', new DateTime('2024-04-01')); $collection = new EventCollection($eventCollectionArray); return new InMemoryEventSource($collection); } private function createEvent(string $id, DateTimeInterface $dateTime): Event { return new Event($id, $dateTime, $this->eventType); } } ``` > **Note: Note** > > When creating the list of events, you must list all the `createEvent()` entities chronologically. > > For example: > > ```php > use App\Calendar\Holidays\Event; > use Ibexa\Contracts\Calendar\EventCollection; > > /** @var \App\Calendar\Holidays\EventType $eventType */ > $collection = new EventCollection([ > new Event('Event 1', new DateTime('2024-01-01'), $eventType), > new Event('Event 2', new DateTime('2024-01-02'), $eventType), > // ... > ]); > ``` Next, register the event source as a service: ```yaml services: App\Calendar\Holidays\EventSourceFactory: arguments: $eventType: '@App\Calendar\Holidays\EventType' App\Calendar\Holidays\EventSource: class: Ibexa\Calendar\EventSource\InMemoryEventSource factory: [ '@App\Calendar\Holidays\EventSourceFactory', 'createEventSource' ] tags: - { name: ibexa.calendar.event.source } ``` Now you can go to the **Calendar** tab and see the configured holiday. ![Custom events list view](https://doc.ibexa.co/en/saas/administration/img/extending_calendar_list_view.png) ### Import events from external sources You can also import events from external sources, for example, a JSON file. To do this, place the following `holidays.json` file in `src/Calendar/Holidays`: ```json [ { "date": "2024-01-01", "name": "New Year Day" }, { "date": "2023-12-25", "name": "Christmas Day" } ] ``` Next, import this file in `src/Calendar/Holidays/EventSourceFactory.php`: ```php public function createEventSource(): EventSourceInterface { $eventCollectionArray = []; $eventCollectionArray[] = $this->createEvent('April Fools', new DateTime('2024-04-01')); $items = json_decode(file_get_contents(__DIR__ . \DIRECTORY_SEPARATOR . 'holidays.json'), true); foreach ($items as $item) { $eventCollectionArray[] = $this->createEvent($item['name'], new DateTime($item['date'])); } $collection = new EventCollection($eventCollectionArray); return new InMemoryEventSource($collection); } ``` The calendar now displays the events listed in the JSON file. # Browser > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). Customize the configuration of the content browser. Browsing the content structure and selecting content from the repository uses the module Universal Discovery Widget (UDW). UDW has an interactive interface which allows you to create, move or copy content items. ## Using UDW UDW requires that you provide configuration by using the `ibexa_udw_config` Twig helper. This configuration must be spread to the props of the component itself. ```html+twig ``` `single` configuration is one of the default configuration provided. You can also do your [own configuration](#add-new-configuration). With plain JS: ```js const container = document.querySelector('#react-udw'); const config = /* fetch the config somewhere */; //const config = JSON.parse(document.querySelector('.btn-udw-trigger).dataset.udwConfig); ReactDOM.render(React.createElement(ibexa.modules.UniversalDiscovery, { onConfirm: {Function}, onCancel: {Function}, ...config }), container); ``` With JSX: ```jsx const props = { onConfirm: {Function}, onCancel: {Function} }; const config = /* fetch the config somewhere */; ``` ## UDW configuration You can configure UDW under the `ibexa.system..universal_discovery_widget_module.configuration` [configuration key](https://doc.ibexa.co/en/saas/administration/configuration/configuration/#configuration-files). There you can set the following properties: | YML React props | Values | Required | Definition | | ------------------------------------------- | ----------------------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------ | | multiple `multiple` | true false | no | The possibility to choose multiple locations. | | multiple_items_limit `multipleItemsLimit` | number | no | Maximum number of items with configuration `multiple: true`. | | root_location_id `rootLocationId` | number | no | UDW displays locations only below this content tree element. | | starting_location_id `startingLocationId` | number | no | This location is displayed as a starting location in UDW. | | containers_only `containersOnly` | true false | no | When set to `true` only containers can be selected. | | allowed_content_types `allowedContentTypes` | null [] \[`contentTypeIdentifier`\] | yes | List of allowed content types: `null` – all content types are allowed, `[]` – empty table, no content types are allowed. | | active_sort_clause `activeSortClause` | DatePublished ContentName | no | Sort Clause by which children in the content tree is sorted. | | active_sort_order `activeSortOrder` | ascending descending | no | Sorting order of the children in the content tree. | | active_tab `activeTab` | browse search bookmarks | no | Starting tab in the UDW. | | active_view `activeView` | finder grid tree | no | Starting view in the UDW. | | allow_redirects `allowRedirects` | true false | yes | Allows to redirect content from the UDW tab to another page, for example, to content edit page. | | selected_locations `selectedLocations` | [] [locationId] | no | Location that is selected automatically. | | allow_confirmation `allowConfirmation` | true false | yes | Shows confirmations buttons in the UDW. If set to false, it's not possible to confirm selection. | ### Content on the Fly group | YML React props | Values | Required | Definition | | ---------------------------------------------------- | -------------------------- | -------- | ---------------------------------------------------------------------------------------------------- | | allowed_languages `allowedLanguages` | null [] [languageCode] | yes | Languages available in Content on the Fly: `null` - all, `[]` - none. | | allowed_locations `allowedLocations` | null [] [locationId] | yes | Location under which creating content is allowed: `null` - everywhere, `[]` - nowhere. | | preselected_language `preselectedLanguage` | null languageCode | yes | First language on the Content on the Fly language list: null - language order defined in the system. | | preselected_content_type `preselectedContentType` | null contentTypeIdentifier | yes | Content selected in Content on the Fly. | | hidden `hidden` | true false | yes | Content on the Fly visibility. | | auto_confirm_after_publish `autoConfirmAfterPublish` | true false | yes | If set to `true` UDW is automatically closed after publishing the content. | ### Tabs config group General configuration for tabs, for example, browse, search, bookmarks. | YML React props | Values | Required | Definition | | ----------------------------- | ---------- | -------- | ----------------------------------------------------------------------------------------- | | items_per_page `itemsPerPage` | number | yes | Number of items shown on one page. | | priority `priority` | number | yes | Priority of items shown in the tab list. Item with a highest value is displayed as first. | | hidden `hidden` | true false | yes | Hides or reveals specific tabs. | ### Configuration available only through JS | React props | Values | Required | Definition | | ----------- | -------- | -------- | ----------------------------------------------------------------------------------------------- | | `onConfirm` | function | yes | A callback to be invoked when a user clicks the confirm button in a Universal Discovery Widget. | | `onCancel` | function | yes | A callback to be invoked when a user clicks the cancel button in a Universal Discovery Widget. | | `title` | string | yes | The title of Universal Discovery Widget. | UDW configuration is SiteAccess-aware. For each defined SiteAccess, you need to be able to use the same configuration tree to define SiteAccess-specific config. These settings need to be mapped to SiteAccess-aware internal parameters that you can retrieve with the [ConfigResolver](https://doc.ibexa.co/en/saas/administration/configuration/dynamic_configuration/#configresolver). ## Add new configuration UDW configuration can change dynamically depending on occurring events. You can use it, for example, to define which content should be exposed to a user after logging in. By default, only one element from configuration file is applied to Universal Discovery Widget. You can modify it dynamically by passing context to generate configuration based on a specific event. This context event is caught by event listener `ConfigResolveEvent::NAME` before the original configuration is used. Depending on what additional parameters are provided, original or event-specific configuration is applied. In the example below `my_custom_udw` is used as a base configuration element for the following steps: ```yaml ibexa: system: : universal_discovery_widget_module: configuration: my_custom_udw: multiple: false ``` ### Add new configuration to button In the `ibexa_udw_config` Twig helper, define a specific part of YAML configuration that is used to render the **Content Browser**. You can find Twig helper in your button template. In the example below, a key is pointing to `my_custom_udw` configuration and has additional parameter `johndoe`. ```html+twig ``` ### Additional parameters If an event listener catches additional parameters passed with context, it uses a configuration specified for it in the event subscriber. In the example below, the `johndoe` parameter enables the user to choose multiple items from a **Browser window** by changing `multiple: false` from `my_custom_udw` configuration to `multiple: true`. ```php The event names to listen to */ public static function getSubscribedEvents(): array { return [ ConfigResolveEvent::NAME => 'onUdwConfigResolve', ]; } public function onUdwConfigResolve(ConfigResolveEvent $event): void { if ($event->getConfigName() !== self::CONFIGURATION_NAME) { return; } $config = $event->getConfig(); $context = $event->getContext(); if (isset($context['some_contextual_parameter'])) { if ($context['some_contextual_parameter'] === 'johndoe') { $config['multiple'] = true; } } $event->setConfig($config); } } ``` For more information, see [Symfony Doctrine Event Listeners and Subscribers tutorial](https://symfony.com/doc/7.4/event_dispatcher.html#creating-an-event-subscriber). # Add browser tab > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). Add a custom tab displaying selected data to the content browser. The Universal Discovery Widget (UDW) is a separate React module. By default, it contains two tabs: Browse and Bookmarks. ![UDW default tabs](https://doc.ibexa.co/en/saas/administration/img/udw_tabs.png) Follow the instructions below to create and add a new tab called **Images** which displays all content items of the type 'Image'. ## Create tab First, in `assets/js/image-tab/`, add an `image.tab.module.js` file. ```js import React, { useContext } from 'react'; import Tab from '@ibexa-admin-ui/src/bundle/ui-dev/src/modules/universal-discovery/components/tab/tab'; import ImagesList from './components/images.list'; const ImageTabModule = () => { return (
    ); }; ``` Next, add the tab to the configuration in the same file. Each tab definition is an object containing the following properties: | Property | Value | Definition | | --------- | ------- | ------------------------------------------------------------------------------------------------------------------- | | id | string | Tab ID, for example, `image`. | | component | element | React component that represents the contents of a tab. | | label | string | Label text, for example, `Images`. | | icon | string | Path to the icon, for example, `/bundles/ibexaadminuiassets/vendors/ids-assets/dist/img/all-icons.svg#info-square`. | ```js ibexa.addConfig( 'adminUiConfig.universalDiscoveryWidget.tabs', [ { id: 'image', component: ImageTabModule, label: 'Image', icon: '/bundles/ibexaadminuiassets/vendors/ids-assets/dist/img/all-icons.svg#image', }, ], true, ); export default ImageTabModule; ``` The module governs the creation of the new tab. Complete image.tab.module.js code ```js import React, { useContext } from 'react'; import Tab from '@ibexa-admin-ui/src/bundle/ui-dev/src/modules/universal-discovery/components/tab/tab'; import ImagesList from './components/images.list'; const ImageTabModule = () => { return (
    ); }; ibexa.addConfig( 'adminUiConfig.universalDiscoveryWidget.tabs', [ { id: 'image', component: ImageTabModule, label: 'Image', icon: '/bundles/ibexaadminuiassets/vendors/ids-assets/dist/img/all-icons.svg#image', }, ], true, ); export default ImageTabModule; ``` ## Add tab to webpack config In `webpack.config.js`, add the following declaration: ```js const ibexaConfigManager = require('./ibexa.webpack.config.manager.js'); ``` Next, provide configuration for the new module: ```js ibexaConfigManager.add({ ibexaConfig, entryName: 'ibexa-admin-ui-udw-tabs-js', newItems: [path.resolve(__dirname, './assets/js/image-tab/image.tab.module.js')], }); ``` ## Provide ReactJS files Next, you need to provide a set of files used to render the module: - `images.service.js` handles fetching the images - `images.list.js` renders the image list - `image.js` renders a single image ### `images.service.js` Create a service for fetching the images by adding `images.service.js` to `assets/js/image-tab/services/`: ```js const handleRequestResponse = (response) => { if (!response.ok) { throw Error(response.statusText); } return response.json(); }; export const getImages = ({ token, siteaccess, contentId }, callback) => { const body = JSON.stringify({ ViewInput: { identifier: 'images', public: false, LocationQuery: { Criteria: {}, FacetBuilders: {}, SortClauses: {}, Filter: { ContentTypeIdCriterion: 5 }, }, }, }); const request = new Request('/api/ibexa/v2/views', { method: 'POST', headers: { Accept: 'application/vnd.ibexa.api.View+json; version=1.1', 'Content-Type': 'application/vnd.ibexa.api.ViewInput+json; version=1.1', 'X-Siteaccess': siteaccess, 'X-CSRF-Token': token, }, body, mode: 'cors', }); fetch(request) .then(handleRequestResponse) .then(callback) .catch((error) => console.log('error:load:images', error)); }; export const loadImageContent = ({ token, siteaccess, contentId }, callback) => { const body = JSON.stringify({ ViewInput: { identifier: `image-content-${contentId}`, public: false, ContentQuery: { Criteria: {}, FacetBuilders: {}, SortClauses: {}, Filter: { ContentIdCriterion: contentId }, }, }, }); const request = new Request('/api/ibexa/v2/views', { method: 'POST', headers: { Accept: 'application/vnd.ibexa.api.View+json; version=1.1', 'Content-Type': 'application/vnd.ibexa.api.ViewInput+json; version=1.1', 'X-Siteaccess': siteaccess, 'X-CSRF-Token': token, }, body, mode: 'cors', }); fetch(request) .then(handleRequestResponse) .then(callback) .catch((error) => console.log('error:load:images', error)); }; ``` ### `images.list.js` Next, create an image list by adding an `images.list.js` to `assets/js/image-tab/components/`: ```js import React, { useState, useContext, useEffect } from 'react'; import Image from './image'; import { getImages } from '../services/images.service'; import { RestInfoContext } from '@ibexa-admin-ui/src/bundle/ui-dev/src/modules/universal-discovery/universal.discovery.module'; const ImagesList = () => { const [images, setImages] = useState([]); const [page, setPage] = useState(0); const [itemsPerPage, setItemPerPage] = useState(5); const [maxPageIndex, setMaxPageIndex] = useState(0); const restInfo = useContext(RestInfoContext); const updateImagesState = (response) => { const images = response.View.Result.searchHits.searchHit.map((item) => item.value.Location); const modulo = images.length % itemsPerPage; const maxPageIndex = modulo ? (images.length - modulo) / itemsPerPage : images.length / itemsPerPage - 1; setImages(images); setMaxPageIndex(maxPageIndex); }; const showPrevPage = () => { const prevPage = page > 0 ? page - 1 : 0; setPage(prevPage); }; const showNextPage = () => { const nextPage = maxPageIndex > page ? page + 1 : maxPageIndex; setPage(nextPage); }; const renderItems = () => { const attrs = { className: 'c-images-list__items', style: { transform: `translate3d(-${page * itemsPerPage * 316}px, 0, 0)`, }, }; return (
    {images.map((imageLocation) => ( ))}
    ); }; const renderPrevBtn = () => { const attrs = { className: 'c-images-list__btn--prev', onClick: showPrevPage, }; if (page <= 0) { attrs.disabled = true; } return (
    ); }; const renderNextBtn = () => { const attrs = { className: 'c-images-list__btn--next', onClick: showNextPage, }; if (page >= maxPageIndex) { attrs.disabled = true; } return (
    ); }; useEffect(() => { getImages(restInfo, updateImagesState); }, []); return (
    {renderPrevBtn()} {renderItems()} {renderNextBtn()}
    ); }; export default ImagesList; ``` ### `image.js` Finally, create an image view by adding an `image.js` to `assets/js/image-tab/components/`: ```js import React, { useState, useEffect, useContext } from 'react'; import { loadImageContent } from '../services/images.service'; import { MarkedLocationIdContext, LoadedLocationsMapContext, MultipleConfigContext, SelectedLocationsContext, } from '@ibexa-admin-ui/src/bundle/ui-dev/src/modules/universal-discovery/universal.discovery.module'; const Image = ({ restInfo, location }) => { const [content, setContent] = useState(null); const [markedLocationId, setMarkedLocationId] = useContext(MarkedLocationIdContext); const [loadedLocationsMap, dispatchLoadedLocationsAction] = useContext(LoadedLocationsMapContext); const [selectedLocations, dispatchSelectedLocationsAction] = useContext(SelectedLocationsContext); const [multiple] = useContext(MultipleConfigContext); const updateVersionInfoState = (response) => { setContent(response.View.Result.searchHits.searchHit[0].value.Content); }; const markLocation = ({ nativeEvent }) => { const isMarkedLocationClicked = location.id === markedLocationId; if (isMarkedLocationClicked) { return; } setMarkedLocationId(location.id); dispatchLoadedLocationsAction({ type: 'CUT_LOCATIONS', locationId: location.id }); dispatchLoadedLocationsAction({ type: 'UPDATE_LOCATIONS', data: { parentLocationId: location.id, subitems: [] } }); if (!multiple) { dispatchSelectedLocationsAction({ type: 'CLEAR_SELECTED_LOCATIONS' }); dispatchSelectedLocationsAction({ type: 'ADD_SELECTED_LOCATION', location }); } }; let src = 'data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBzdGFuZGFsb25lPSJubyI/Pgo8IURPQ1RZUEUgc3ZnIFBVQkxJQyAiLS8vVzNDLy9EVEQgU1ZHIDEuMS8vRU4iICJodHRwOi8vd3d3LnczLm9yZy9HcmFwaGljcy9TVkcvMS4xL0RURC9zdmcxMS5kdGQiPgo8c3ZnIHdpZHRoPSI0MHB4IiBoZWlnaHQ9IjQwcHgiIHZpZXdCb3g9IjAgMCA0MCA0MCIgdmVyc2lvbj0iMS4xIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiB4bWw6c3BhY2U9InByZXNlcnZlIiBzdHlsZT0iZmlsbC1ydWxlOmV2ZW5vZGQ7Y2xpcC1ydWxlOmV2ZW5vZGQ7c3Ryb2tlLWxpbmVqb2luOnJvdW5kO3N0cm9rZS1taXRlcmxpbWl0OjEuNDE0MjE7IiB4PSIwcHgiIHk9IjBweCI+CiAgICA8ZGVmcz4KICAgICAgICA8c3R5bGUgdHlwZT0idGV4dC9jc3MiPjwhW0NEQVRBWwogICAgICAgICAgICBALXdlYmtpdC1rZXlmcmFtZXMgc3BpbiB7CiAgICAgICAgICAgICAgZnJvbSB7CiAgICAgICAgICAgICAgICAtd2Via2l0LXRyYW5zZm9ybTogcm90YXRlKDBkZWcpCiAgICAgICAgICAgICAgfQogICAgICAgICAgICAgIHRvIHsKICAgICAgICAgICAgICAgIC13ZWJraXQtdHJhbnNmb3JtOiByb3RhdGUoLTM1OWRlZykKICAgICAgICAgICAgICB9CiAgICAgICAgICAgIH0KICAgICAgICAgICAgQGtleWZyYW1lcyBzcGluIHsKICAgICAgICAgICAgICBmcm9tIHsKICAgICAgICAgICAgICAgIHRyYW5zZm9ybTogcm90YXRlKDBkZWcpCiAgICAgICAgICAgICAgfQogICAgICAgICAgICAgIHRvIHsKICAgICAgICAgICAgICAgIHRyYW5zZm9ybTogcm90YXRlKC0zNTlkZWcpCiAgICAgICAgICAgICAgfQogICAgICAgICAgICB9CiAgICAgICAgICAgIHN2ZyB7CiAgICAgICAgICAgICAgICAtd2Via2l0LXRyYW5zZm9ybS1vcmlnaW46IDUwJSA1MCU7CiAgICAgICAgICAgICAgICAtd2Via2l0LWFuaW1hdGlvbjogc3BpbiAxLjVzIGxpbmVhciBpbmZpbml0ZTsKICAgICAgICAgICAgICAgIC13ZWJraXQtYmFja2ZhY2UtdmlzaWJpbGl0eTogaGlkZGVuOwogICAgICAgICAgICAgICAgYW5pbWF0aW9uOiBzcGluIDEuNXMgbGluZWFyIGluZmluaXRlOwogICAgICAgICAgICB9CiAgICAgICAgXV0+PC9zdHlsZT4KICAgIDwvZGVmcz4KICAgIDxnIGlkPSJvdXRlciI+CiAgICAgICAgPGc+CiAgICAgICAgICAgIDxwYXRoIGQ9Ik0yMCwwQzIyLjIwNTgsMCAyMy45OTM5LDEuNzg4MTMgMjMuOTkzOSwzLjk5MzlDMjMuOTkzOSw2LjE5OTY4IDIyLjIwNTgsNy45ODc4MSAyMCw3Ljk4NzgxQzE3Ljc5NDIsNy45ODc4MSAxNi4wMDYxLDYuMTk5NjggMTYuMDA2MSwzLjk5MzlDMTYuMDA2MSwxLjc4ODEzIDE3Ljc5NDIsMCAyMCwwWiIgc3R5bGU9ImZpbGw6YmxhY2s7Ii8+CiAgICAgICAgPC9nPgogICAgICAgIDxnPgogICAgICAgICAgICA8cGF0aCBkPSJNNS44NTc4Niw1Ljg1Nzg2QzcuNDE3NTgsNC4yOTgxNSA5Ljk0NjM4LDQuMjk4MTUgMTEuNTA2MSw1Ljg1Nzg2QzEzLjA2NTgsNy40MTc1OCAxMy4wNjU4LDkuOTQ2MzggMTEuNTA2MSwxMS41MDYxQzkuOTQ2MzgsMTMuMDY1OCA3LjQxNzU4LDEzLjA2NTggNS44NTc4NiwxMS41MDYxQzQuMjk4MTUsOS45NDYzOCA0LjI5ODE1LDcuNDE3NTggNS44NTc4Niw1Ljg1Nzg2WiIgc3R5bGU9ImZpbGw6cmdiKDIxMCwyMTAsMjEwKTsiLz4KICAgICAgICA8L2c+CiAgICAgICAgPGc+CiAgICAgICAgICAgIDxwYXRoIGQ9Ik0yMCwzMi4wMTIyQzIyLjIwNTgsMzIuMDEyMiAyMy45OTM5LDMzLjgwMDMgMjMuOTkzOSwzNi4wMDYxQzIzLjk5MzksMzguMjExOSAyMi4yMDU4LDQwIDIwLDQwQzE3Ljc5NDIsNDAgMTYuMDA2MSwzOC4yMTE5IDE2LjAwNjEsMzYuMDA2MUMxNi4wMDYxLDMzLjgwMDMgMTcuNzk0MiwzMi4wMTIyIDIwLDMyLjAxMjJaIiBzdHlsZT0iZmlsbDpyZ2IoMTMwLDEzMCwxMzApOyIvPgogICAgICAgIDwvZz4KICAgICAgICA8Zz4KICAgICAgICAgICAgPHBhdGggZD0iTTI4LjQ5MzksMjguNDkzOUMzMC4wNTM2LDI2LjkzNDIgMzIuNTgyNCwyNi45MzQyIDM0LjE0MjEsMjguNDkzOUMzNS43MDE5LDMwLjA1MzYgMzUuNzAxOSwzMi41ODI0IDM0LjE0MjEsMzQuMTQyMUMzMi41ODI0LDM1LjcwMTkgMzAuMDUzNiwzNS43MDE5IDI4LjQ5MzksMzQuMTQyMUMyNi45MzQyLDMyLjU4MjQgMjYuOTM0MiwzMC4wNTM2IDI4LjQ5MzksMjguNDkzOVoiIHN0eWxlPSJmaWxsOnJnYigxMDEsMTAxLDEwMSk7Ii8+CiAgICAgICAgPC9nPgogICAgICAgIDxnPgogICAgICAgICAgICA8cGF0aCBkPSJNMy45OTM5LDE2LjAwNjFDNi4xOTk2OCwxNi4wMDYxIDcuOTg3ODEsMTcuNzk0MiA3Ljk4NzgxLDIwQzcuOTg3ODEsMjIuMjA1OCA2LjE5OTY4LDIzLjk5MzkgMy45OTM5LDIzLjk5MzlDMS43ODgxMywyMy45OTM5IDAsMjIuMjA1OCAwLDIwQzAsMTcuNzk0MiAxLjc4ODEzLDE2LjAwNjEgMy45OTM5LDE2LjAwNjFaIiBzdHlsZT0iZmlsbDpyZ2IoMTg3LDE4NywxODcpOyIvPgogICAgICAgIDwvZz4KICAgICAgICA8Zz4KICAgICAgICAgICAgPHBhdGggZD0iTTUuODU3ODYsMjguNDkzOUM3LjQxNzU4LDI2LjkzNDIgOS45NDYzOCwyNi45MzQyIDExLjUwNjEsMjguNDkzOUMxMy4wNjU4LDMwLjA1MzYgMTMuMDY1OCwzMi41ODI0IDExLjUwNjEsMzQuMTQyMUM5Ljk0NjM4LDM1LjcwMTkgNy40MTc1OCwzNS43MDE5IDUuODU3ODYsMzQuMTQyMUM0LjI5ODE1LDMyLjU4MjQgNC4yOTgxNSwzMC4wNTM2IDUuODU3ODYsMjguNDkzOVoiIHN0eWxlPSJmaWxsOnJnYigxNjQsMTY0LDE2NCk7Ii8+CiAgICAgICAgPC9nPgogICAgICAgIDxnPgogICAgICAgICAgICA8cGF0aCBkPSJNMzYuMDA2MSwxNi4wMDYxQzM4LjIxMTksMTYuMDA2MSA0MCwxNy43OTQyIDQwLDIwQzQwLDIyLjIwNTggMzguMjExOSwyMy45OTM5IDM2LjAwNjEsMjMuOTkzOUMzMy44MDAzLDIzLjk5MzkgMzIuMDEyMiwyMi4yMDU4IDMyLjAxMjIsMjBDMzIuMDEyMiwxNy43OTQyIDMzLjgwMDMsMTYuMDA2MSAzNi4wMDYxLDE2LjAwNjFaIiBzdHlsZT0iZmlsbDpyZ2IoNzQsNzQsNzQpOyIvPgogICAgICAgIDwvZz4KICAgICAgICA8Zz4KICAgICAgICAgICAgPHBhdGggZD0iTTI4LjQ5MzksNS44NTc4NkMzMC4wNTM2LDQuMjk4MTUgMzIuNTgyNCw0LjI5ODE1IDM0LjE0MjEsNS44NTc4NkMzNS43MDE5LDcuNDE3NTggMzUuNzAxOSw5Ljk0NjM4IDM0LjE0MjEsMTEuNTA2MUMzMi41ODI0LDEzLjA2NTggMzAuMDUzNiwxMy4wNjU4IDI4LjQ5MzksMTEuNTA2MUMyNi45MzQyLDkuOTQ2MzggMjYuOTM0Miw3LjQxNzU4IDI4LjQ5MzksNS44NTc4NloiIHN0eWxlPSJmaWxsOnJnYig1MCw1MCw1MCk7Ii8+CiAgICAgICAgPC9nPgogICAgPC9nPgo8L3N2Zz4K'; let alt = 'Loading meta data ...'; useEffect(() => { loadImageContent({ ...restInfo, contentId: location.ContentInfo.Content._id }, updateVersionInfoState); }, []); if (content) { const imageField = content.CurrentVersion.Version.Fields.field.find( (field) => field.fieldTypeIdentifier === 'ibexa_image', ).fieldValue; src = imageField.uri; alt = imageField.fileName; } return (
    {alt}
    ); }; export default Image; ``` ## Add styles Ensure that the new tab is styled by adding the following files to `assets/css/`. ### `images.list.css` ```css .c-images-list { display: grid; grid-template-areas: 'prev list next'; grid-template-columns: 32px 1fr 32px; grid-gap: 16px; overflow: hidden; } .c-images-list__items-wrapper { overflow: hidden; max-width: 1564px; } [class*='c-images-list__btn--'] { display: flex; align-items: center; justify-content: center; background: #f15a10; transition: background 0.2s ease-in-out, opacity 0.2s ease-in-out; } [class*='c-images-list__btn--']:focus, [class*='c-images-list__btn--']:hover { background: #ab3f0a; } [class*='c-images-list__btn--'][disabled], [class*='c-images-list__btn--'][disabled]:focus, [class*='c-images-list__btn--'][disabled]:hover { background: #f15a10; opacity: 0.5; } [class*='c-images-list__btn--'] .ibexa-icon { fill: #fff; } .c-images-list__btn--prev { grid-area: prev; } .c-images-list__btn--next { grid-area: next; } .c-images-list__items { grid-area: list; display: flex; flex-wrap: nowrap; transition: transform 0.3s ease-in-out; } .c-images-list__items .c-image { flex: 0 0 300px; } .c-images-list__items .c-image + .c-image { margin-left: 1rem; } ``` ### `image.css` ```css .c-image { width: 300px; height: 200px; background: #fff; transition: box-shadow 0.3s ease-in-out; position: relative; cursor: pointer; display: flex; } .c-image:before { content: attr(data-title); display: flex; background: rgba(0, 0, 0, 0.75); color: #fff; width: 300px; align-items: center; justify-content: center; font-weight: 700; position: absolute; top: 0; left: 0; right: 0; bottom: 0; opacity: 0; padding: 1rem; transition: opacity 0.3s ease-in-out; overflow: hidden; } .c-image:hover:before, .c-image:focus:before { opacity: 1; } .c-image__thumb { display: block; max-width: 300px; max-height: 200px; width: auto; height: auto; margin: auto; } ``` ### Add CSS to webpack Finally, add CSS in `webpack.config.js`: ```js ibexaConfigManager.add({ ibexaConfig, entryName: 'ibexa-admin-ui-layout-css', newItems: [path.resolve(__dirname, './assets/css/image.css'), path.resolve(__dirname, './assets/css/image.list.css')], }); ``` ## Check results In the back office go to **Content** -> **Dashboard**. On the top right, click the **Create content** button. In the UDW a new **Images** tab appears, listing all images from the repository. ![Image tab in UDW](https://doc.ibexa.co/en/saas/administration/img/udw_image_tab.png) > **Tip: Tip** > > If you cannot see the results or encounter an error, clear the cache and reload the application. Remember, after any change of css/js files you should always run `yarn encore dev` in the terminal. # Multi-file upload > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). Configure multi-file upload functionality which allows uploading files in bulk. You can use the multi-file upload module in the editorial interface of Cohesivo. It provides an interface to publish content based on dropped files while uploading them in the interface. > **Caution: Caution** > > If you want to load the multi-file upload module, you need to load the JS code for it in your view, as it's not available by default. ## Use multi-file upload With JS only: ```js React.createElement(ibexa.modules.MultiFileUpload, { onAfterUpload: {Function}, adminUiConfig: { multiFileUpload: { defaultMappings: [{ contentTypeIdentifier: {String}, contentFieldIdentifier: {String}, contentNameIdentifier: {String}, mimeTypes: [{String}, {String}, ...] }], fallbackContentType: { contentTypeIdentifier: {String}, contentFieldIdentifier: {String}, contentNameIdentifier: {String} }, locationMappings: [{Object}], maxFileSize: {Number} }, token: {String}, siteaccess: {String} }, parentInfo: { contentTypeIdentifier: {String}, contentTypeId: {Number}, locationPath: {String}, language: {String} } }); ``` With JSX: ```jsx const attrs = { onAfterUpload: {Function}, adminUiConfig: { multiFileUpload: { defaultMappings: [{ contentTypeIdentifier: {String}, contentFieldIdentifier: {String}, contentNameIdentifier: {String}, mimeTypes: [{String}, {String}, ...] }], fallbackContentType: { contentTypeIdentifier: {String}, contentFieldIdentifier: {String}, contentNameIdentifier: {String} }, locationMappings: [{Object}], maxFileSize: {Number} }, token: {String}, siteaccess: {String} }, parentInfo: { contentTypeIdentifier: {String}, contentTypeId: {Number}, locationPath: {String}, language: {String} } }; ``` ## Properties list The `` module can handle additional properties. There are two types of properties: **required** and **optional**. ### Required properties All of the following properties must be used, otherwise the multi-file upload doesn't work. - **onAfterUpload** *{Function}* - a callback to be invoked immediately after a file has been uploaded - **adminUiConfig** *{Object}* - UI config object. It should keep the following structure: - **multiFileUpload** *{Object}* - multi file upload module config: - **defaultMappings** *{Array}* - a list of file type to content type mappings Sample mapping be an object and should follow the convention: - **contentTypeIdentifier** *{String}* - content type identifier - **contentFieldIdentifier** *{String}* - field identifier - **nameFieldIdentifier** *{String}* - name field identifier - **mimeTypes** *{Array}* - a list of file types assigned to a specific content type - **fallbackContentType** *{Object}* - a fallback content type definition. Should contain the following info: - **contentTypeIdentifier** *{String}* - content type identifier - **contentFieldIdentifier** *{String}* - field identifier - **nameFieldIdentifier** *{String}* - name field identifier - **locationMappings** *{Array}* - list of file type to content type mappings based on a location identifier - **maxFileSize** {Number} - maximum file size allowed for uploading. It's a number of bytes - **token** *{String}* - CSRF token - **siteaccess** *{String}* - SiteAccess identifier - **parentInfo** *{Object}* - parent location meta information: - **contentTypeIdentifier** *{String}* - content type identifier - **contentTypeId** *{Number}* - content type ID - **locationPath** *{String}* - location path string - **language** *{String}* - language code identifier ### Optional properties Optionally, the multi-file upload module can take a following list of properties: - **checkCanUpload** *{Function}* - checks whether am uploaded file can be uploaded. The callback takes four params: - **file** *{File}* - file object - **parentInfo** *{Object}* - parent location meta information - **config** *{Object}* - Multi-file Upload module config - **callbacks** *{Object}* - error callbacks list: **fileTypeNotAllowedCallback** and **fileSizeNotAllowedCallback** - **createFileStruct** *{Function}* - a function that creates a *ContentCreate* struct. The function takes two params: - **file** *{File}* - file object - **params** *{Object}* - params hash containing: **parentInfo** and **adminUiConfig** stored under the **config** key - **deleteFile** *{Function}* - a function deleting content created from a given file. It takes three params: - **systemInfo** *{Object}* - hash containing information about CSRF token and SiteAccess: **token** and **siteaccess** - **struct** *{Object}* - content struct - **callback** *{Function}* - content deleted callback - **onPopupClose** *{Function}* - function invoked when closing a Multi-file Upload popup. It takes one param: **itemsUploaded** - the list of uploaded items - **publishFile** *{Function}* - publishes an uploaded file-based content item. Takes three params: - **data** *{Object}* - an object containing information about: - **struct** *{Object}* - the ContentCreate struct () - **token** *{String}* - CSRF token - **siteaccess** *{String}* - SiteAccess identifier - **requestEventHandlers** *{Object}* - a list of upload event handlers: - **onloadstart** *{Function}* - on load start callback - **upload** *{Object}* - file upload events: - **onabort** *{Function}* - on abort callback - **onload** *{Function}* - on load callback - **onprogress** *{Function}* - on progress callback - **ontimeout** *{Function}* - on timeout callback - **callback** *{Function}* - a callback invoked when an uploaded file-based content has been published # Sub-items list > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). Inject a sub-items list into your back office customizations or customize the view. The Sub-items List module is meant to be used as a part of the editorial interface of Cohesivo. It provides an interface for listing the sub-items of any location. ## Create custom sub-items list view You can extend the Sub-items List module to replace an existing view or add your own. The example below adds a new timeline view to highlight the modification date. ![Sub-items List module using the new Timeline view](https://doc.ibexa.co/en/saas/administration/back_office/img/subitems/timeline_view.png "Sub-items List module using the new Timeline view") To recreate it, start by creating the components responsible for rendering the new view. You can create two files: - `assets/js/timeline.view.component.js` responsible for rendering the whole view ```js import React from 'react'; import PropTypes from 'prop-types'; import TimelineViewItemComponent from './timeline.view.item.component'; const TimelineViewComponent = ({ items, generateLink }) => { const groupByDate = (items) => { return items.reduce((groups, item) => { const date = new Date(item.content._info.modificationDate.timestamp * 1000); const dateKey = date.toISOString().split('T')[0]; if (!groups[dateKey]) { groups[dateKey] = []; } groups[dateKey].push(item); return groups; }, {}); }; const groupedItems = groupByDate(items); return (
    {Object.entries(groupedItems).map(([date, dateItems]) => (

    {new Date(date).toLocaleDateString()}

    {dateItems.map((item) => ( ))}
    ))}
    ); }; TimelineViewComponent.propTypes = { items: PropTypes.array.isRequired, generateLink: PropTypes.func.isRequired, }; export default TimelineViewComponent; ``` - `assets/js/timeline.view.item.component.js` responsible for rendering a single item ```js import React from 'react'; import PropTypes from 'prop-types'; import Icon from '@ibexa-admin-ui-modules/common/icon/icon'; const { ibexa } = window; const TimelineViewItemComponent = ({ item, generateLink }) => { const { content } = item; const contentTypeIdentifier = content._info.contentType.identifier; const contentTypeIconUrl = ibexa.helpers.contentType.getContentTypeIconUrl(contentTypeIdentifier); const time = new Date(content._info.modificationDate.timestamp * 1000).toLocaleTimeString(); return (
    {time}
    {content._name}
    {content._info.contentType.name}
    ); }; TimelineViewItemComponent.propTypes = { item: PropTypes.object.isRequired, generateLink: PropTypes.func.isRequired, }; export default TimelineViewItemComponent; ``` Provide the necessary styling in `assets/scss/timeline.view.scss`. The example below uses Cohesivo's SCSS variables for consistency with the rest of the back office interface. ```scss @use '@ibexa-admin-ui/src/bundle/Resources/public/scss/custom.scss' as *; .app-timeline-view { padding: calculateRem(16px); &__group { position: relative; margin-bottom: calculateRem(32px); } &__date { display: flex; align-items: center; margin-bottom: calculateRem(16px); h3 { margin: 0; font-size: $ibexa-text-font-size-large; color: $ibexa-color-dark; } } &__date-marker { width: calculateRem(12px); height: calculateRem(12px); border-radius: 50%; background: $ibexa-color-primary; margin-right: calculateRem(16px); } &__items { margin-left: calculateRem(6px); padding-left: calculateRem(32px); border-left: calculateRem(2px) solid $ibexa-color-light; } } .app-timeline-view-item { display: flex; align-items: flex-start; padding: calculateRem(16px); margin-bottom: calculateRem(8px); text-decoration: none; color: inherit; background: $ibexa-color-light-300; border-radius: $ibexa-border-radius; transition: background-color 0.2s $ibexa-admin-transition; &:hover { background: $ibexa-color-light-400; } &__time { color: $ibexa-color-dark-400; margin-right: calculateRem(16px); min-width: calculateRem(80px); } &__content { display: flex; align-items: center; } &__icon { margin-right: calculateRem(16px); } &__name { font-weight: $ibexa-font-weight-bold; margin-bottom: calculateRem(4px); } &__type { font-size: $ibexa-text-font-size-small; color: $ibexa-color-dark-400; display: flex; align-items: center; gap: calculateRem(8px); } &__type-name { line-height: calculateRem(16px); } } ``` The last step is adding the view module to the list of available views in the system, by using the provided `registerView` function. You can create a new view by providing an unique identifier, or replace an existing one by reusing its identifier. The existing view identifiers are defined as JavaScript constants in the `@ibexa-admin-ui-modules/sub-items/constants` module: - Grid view: `VIEW_MODE_GRID` constant - Table view: `VIEW_MODE_TABLE` constant Create a file called `assets/js/registerTimelineView.js`: ```js import TimelineViewComponent from './timeline.view.component.js'; import { registerView } from '@ibexa-admin-ui-modules/sub-items/services/view.registry'; // Use the existing constants to replace a view import { VIEW_MODE_GRID, VIEW_MODE_TABLE } from '@ibexa-admin-ui-modules/sub-items/constants'; registerView('timeline', { component: TimelineViewComponent, iconName: 'timeline', label: 'Timeline view', }); ``` And include it into the back office using Webpack Encore, together with your custom styles. See [configuring assets from main project files](https://doc.ibexa.co/en/saas/administration/back_office/back_office_elements/importing_assets_from_bundle/#configuration-from-main-project-files) to learn more about this mechanism. ```js const ibexaConfigManager = require('./ibexa.webpack.config.manager.js'); //... ibexaConfigManager.add({ ibexaConfig, entryName: 'ibexa-admin-ui-layout-js', newItems: [ path.resolve(__dirname, './assets/js/registerTimelineView.js') ], }); ibexaConfigManager.add({ ibexaConfig, entryName: 'ibexa-admin-ui-layout-css', newItems: [ path.resolve(__dirname, './assets/scss/timeline.view.scss'), ], }); ``` Complete the task by running `composer run post-install-cmd`. ## Use sub-items list > **Caution: Caution** > > If you want to load the Sub-items module from your custom code, you need to load the JS code for it in your view, as it's not available by default. With plain JS: ```js const containerNode = document.querySelector('#sub-items-container'); ReactDOM.render( React.createElement(ibexa.modules.SubItems, { parentLocationId: { Number }, restInfo: { token: { String }, siteaccess: { String }, }, }), containerNode, ); ``` With JSX: ```jsx const attrs = { parentLocationId: {Number}, restInfo: { token: {String}, siteaccess: {String} } }; ``` ## Properties list The `` module can handle additional properties. There are two types of properties: **required** and **optional**. All of them are listed below. ### Required props Without all the following properties the Sub-items module cannot work. - **parentLocationId** *{Number}* - parent location ID - **restInfo** *{Object}* - backend config object: - **token** *{String}* - CSRF token - **siteaccess** *{String}* - SiteAccess identifier - **handleEditItem** *{Function}* - callback to handle edit content action - **generateLink** *{Function}* - callback to handle view content action ### Optional properties Optionally, Sub-items module can take a following list of props: - **loadContentInfo** *{Function}* - loads content item info. Takes two params: - **contentIds** *{Array}* - list of content IDs - **callback** *{Function}* - a callback invoked when content info is loaded - **loadContentTypes** *{Function}* - loads content types. Takes one param: - **callback** *{Function}* - callback invoked when content types are loaded - **loadLocation** *{Function}* - loads location. Takes four params: - **restInfo** *{Object}* - REST info params: - **token** *{String}* - the user token - **siteaccess** *{String}* - the current SiteAccess - **queryConfig** *{Object}* - query config: - **locationId** *{Number}* - location ID - **limit** *{Number}* - content item limit - **offset** *{Number}* - items offset - **sortClauses** *{Object}* - the Sort Clauses, for example, {LocationPriority: 'ascending'} - **callback** *{Function}* - callback invoked when location is loaded - **updateLocationPriority** - updates item location priority. Takes two params: - **params** *{Object}* - parameters hash containing: - **priority** *{Number}* - priority value - **location** *{String}* - REST location ID - **token** *{String}* - CSRF token - **siteaccess** *{String}* - SiteAccess identifier - **callback** *{Function}* - callback invoked when location priority is updated - **activeView** *{String}* - active list view identifier - **extraActions** *{Array}* - list of extra actions. Each action is an object containing: - **component** *{Element}* - React component class - **attrs** *{Object}* - additional component properties - **items** *{Array}* - list of location's sub-items - **limit** *{Number}* - items limit count - **offset** *{Number}* - items limit offset - **labels** *{Object}* - list of module labels, see [sub.items.module.js](https://github.com/ibexa/admin-ui/blob/6.0/src/bundle/ui-dev/src/modules/sub-items/sub.items.module.js) for details. Contains definitions for sub components: - **subItems** *{Object}* - list of sub-items module labels - **tableView** *{Object}* - list of table view component labels - **tableViewItem** *{Object}* - list of table item view component labels - **loadMore** *{Object}* - list of load more component labels - **gridViewItem** *{Object}* - list of grid item view component labels - **languageContainerSelector** *{String}* - selector where the language selector should be rendered ## Reuse Sub-items list To add a Sub-items list on a page that doesn't have the (right) action sidebar, you need to do one of the following things: - add a `
    ` element with the `.ibexa-extra-actions-container` selector - change the selector in the Sub-items settings by sending the `languageContainerSelector` prop which takes the selector for the element that renders the `languageSelector`. # Notifications > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). You can send notifications to users who work with the back office by using notification bars or notifications in the user menu. You can send two types of notifications to the users: - [Notification bar](#notification-bars) is displayed in specific situations as a message bar appearing at the bottom of the page. It appears to whoever is doing a specific operation in the back office. - [User notifications](#user-notifications) are sent to a specific user. They appear in their profile in the back office. ## Notification bars Notifications are displayed as a message bar in the back office. There are four types of notifications: `info`, `success`, `warning` and `error`. ![Screenshot of a notification bar](https://doc.ibexa.co/en/saas/administration/img/notification2.png "Example of notification bar") ### Display notification bar from PHP To send a notification from PHP, inject the `TranslatableNotificationHandlerInterface` into your class. ```php /** @var \Ibexa\Contracts\AdminUi\Notification\TranslatableNotificationHandlerInterface $notificationHandler */ $notificationHandler->info( /** @Desc("Notification text") */ 'example.notification.text', [], 'domain' ); ``` To have the notification translated, provide the message strings in the translation files under the correct domain and key. ### Display notification bar from front end To create a notification from the front end (in this example, of type `info`), use the following code: ```js const eventInfo = new CustomEvent('ibexa-notify', { detail: { label: 'info', message: 'Notification text' } }); ``` Dispatch the event with `document.body.dispatchEvent(eventInfo);`. ### Notification bar timeout To define the timeout for hiding Back-Office notification bars, per notification type, use the `ibexa.system..notifications..timeout` [configuration key](https://doc.ibexa.co/en/saas/administration/configuration/configuration/#configuration-files): ```yaml ibexa: system: admin: notifications: error: timeout: 0 warning: timeout: 0 success: timeout: 5000 info: timeout: 0 ``` The values shown above are the defaults. `0` means the notification doesn't hide automatically. ### `browser` notification channel To send notification bars, you can also subscribe to a notification with the `browser` channel. ## User notifications You can send notifications to users which are displayed in the user menu. ![Screenshot of the user menu with an highlight on the bell icon](https://doc.ibexa.co/en/saas/administration/img/notification3.png "Profile notification bell menu") ### Create a custom user notification To create a new notification you can use the [`NotificationService::createNotification(CreateStruct $createStruct)` method](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-Core-Repository-NotificationService.html#method_createNotification) like in the example below: ```php 'onPublishVersion']; } public function onPublishVersion(PublishVersionEvent $event): void { $data = [ 'content_name' => $event->getContent()->getName(), 'content_id' => $event->getContent()->id, 'message' => 'published', ]; $notification = new CreateStruct(); $notification->ownerId = $event->getContent()->contentInfo->ownerId; $notification->type = 'ContentPublished'; $notification->data = $data; $this->notificationService->createNotification($notification); } } ``` A new type of user notification is created: `ContentPublished`. ### Display a custom user notification To display a user notification, write a renderer and tag it as a service. The example below presents a renderer that uses Twig to render a view: ```php twig->render('@ibexadesign/notification.html.twig', [ 'notification' => $notification, 'template_to_extend' => $templateToExtend, ]); } public function generateUrl(Notification $notification): ?string { if (array_key_exists('content_id', $notification->data)) { return $this->router->generate('ibexa.content.view', ['contentId' => $notification->data['content_id']]); } return null; } public function getTypeLabel(): string { return /** @Desc("Workflow stage changed") */ $this->translator->trans( 'workflow.notification.stage_change.label', [], 'ibexa_workflow' ); } } ``` You can add the template that is used in the `MyRenderer::render()` method to the `admin` theme as `templates/themes/admin/notification.html.twig`: ```html+twig {% extends template_to_extend %} {% trans_default_domain 'custom_notification' %} {% set wrapper_additional_classes = 'css-class-custom' %} {% block icon %} {% endblock %} {% block notification_type %} {{ 'Notice'|trans|desc('Notice') }} {% endblock %} {% block message %} {% embed '@ibexadesign/ui/component/table/table_body_cell.html.twig' with { class: 'ibexa-notifications-modal__description' } %} {% block content %}

    {{ notification.data.content_name }} {{ notification.data.message }}

    {% endblock %} {% endembed %} {% endblock %} ``` Finally, you need to add an entry to `config/services.yaml` to tag and bound the renderer service to the `ContentPublished` type: ```yaml services: App\Notification\MyRenderer: tags: - { name: ibexa.notification.renderer, alias: ContentPublished } ``` ### Display notification list To display a list of notifications, expand the above renderer. The example below presents a modified renderer that uses Twig to render a list view: ```php requestStack->getCurrentRequest(); if ($currentRequest && $currentRequest->attributes->getBoolean('render_all')) { $templateToExtend = '@ibexadesign/account/notifications/list_item_all.html.twig'; } return $this->twig->render('@ibexadesign/notification.html.twig', [ 'notification' => $notification, 'template_to_extend' => $templateToExtend, ]); } public function generateUrl(Notification $notification): ?string { if (array_key_exists('content_id', $notification->data)) { return $this->router->generate('ibexa.content.view', [ 'contentId' => $notification->data['content_id'], ]); } return null; } public function getTypeLabel(): string { return /** @Desc("Workflow stage changed") */ $this->translator->trans( 'workflow.notification.stage_change.label', [], 'ibexa_workflow' ); } } ``` ### `ibexa` notification channel To send user notifications, you can also subscribe to a notification with the `ibexa` channel. # Integrated help > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). Integrated help provides quick access to documentation, training, and support resources. Editions: LTS Update Integrated help is an [LTS Update](https://doc.ibexa.co/en/saas/ibexa_products/editions/#lts-updates) that brings documentation, training resources, and product roadmap-related information into the back office, together with user onboarding capabilities. With this feature installed, users can click the ![Help icon](https://doc.ibexa.co/en/saas/administration/back_office/img/about-info.png) icon to access relevant content straight from the UI. ![Integrated help menu](https://doc.ibexa.co/en/saas/administration/back_office/img/5_0_integrated_help_menu.png) Integrated help is contextual, therefore, apart from user documentation, release notes, and partner guidelines, which are available to editors and store managers, developers can access API references, the GraphQL console, or the support portal. ## Product tours Product tours are interactive guided walkthroughs that help back office users discover Cohesivo features, available starting with Cohesivo v4.6.29. They provide step-by-step guidance directly within the application interface, accelerating user adoption and reducing training time. Developers can create custom onboarding journeys tailored to specific client implementations, user roles, or business processes. For more information, see [Product tour](https://doc.ibexa.co/en/saas/administration/back_office/product_tour/index.md). ## Install package The Integrated help LTS Update is optional. To enable it, run the following command: ```bash composer require ibexa/integrated-help ``` After installation, the help center is enabled by default for all back office users. If needed, they can [disable it in user settings](https://doc.ibexa.co/projects/userguide/en/6.0/getting_started/discover_ui/#disable-help-center). # Customize integrated help > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). Customize the integrated help menu. Editions: LTS Update The integrated help menu is part of the Integrated help introduced as an [LTS Update](https://doc.ibexa.co/en/saas/ibexa_products/editions/#lts-updates). By default, it provides editors and developers with convenient access to documentation, training and other resources directly from the back office. You can extend or modify the integrated menu in the following ways: - by disabling it for all users - by modifying a link to user documentation - by subscribing to the `ibexa_integrated_help.menu_configure.help_menu` event ## Disable integrated help functionalities After you have installed the integrated help package, you can disable the entire feature or specific functionalities on the system level. ### Disable all functionalities To disable both the Help center and the Product tour globally, for example, to run UI tests in a `dev` [environment](https://doc.ibexa.co/en/saas/infrastructure_and_maintenance/environments/index.md), in `config/packages`, create the `ibexa_integrated_help.yaml` file with the following configuration: ```yaml ibexa_integrated_help: enabled: false ``` ### Disable functionalities independently To disable only the Help center or only the Product tour functionalities, use the dedicated flags as in the example below: ```yaml ibexa_integrated_help: help_center: enabled: false # Disable only the Help center product_tour: enabled: false # Disable only the Product tour ``` ## Modify user documentation link Cohesivo provides a comfortable method for replacing a link to user documentation, when you do not want to modify the rest of the integrated help menu. This way you can direct application users such as editors or store managers to specific guidelines in force at your organization, without having to resort to development. To do it, in `config/packages` create the `ibexa_integrated_help.yaml` file, with the following configuration: ```yaml ibexa_integrated_help: user_documentation: ``` ## Intercept and modify event Cohesivo uses [KnpMenuBundle](https://github.com/KnpLabs/KnpMenuBundle) to build its backend menus. When it builds the integrated help menu, it dispatches the `ibexa_integrated_help.menu_configure.help_menu` event to pass information about the contents of the help menu to the front end. You can intercept this event, and change its contents by creating a subscriber. With that subscriber, you can access the `menu` object, which is an instance of the `Knp\Menu\MenuItem`, and all the options passed by this object, and modify them. This way you can adjust menu sections that are reproduced by the front end as tabs, add new items, or integrate custom links into the help system. ### Menu object structure The default `menu` object is structured as follows. Recreate this pattern when modifying an existing event with an intention to send it to the front end. ```text root (MenuItem) │ ├── help__general // ("General" section) │ ├── help__user_documentation // (User docs, highlighted menu option) │ │ (...) │ └── help__submit_idea // (Submit idea, regular option) │ └── help__developers // (conditional "Developers" section) ├── help__developer_documentation // (Developer docs, highlighted) │ (...) └── help__support_portal ``` `help_general` and `help_developers` are menu sections, or tabs. Sections consist of entries, and each entry carries the following information: - `label` - a name of the help menu item - `uri` - an external link to the resource - `isHighlighted` - a Boolean switch that decides whether the menu item should be placed at the top of the tab - `icon` - a link to a graphic file to accompany the menu item - `description` - a summary of what users can expect after clicking the menu item ### Create a subscriber Build a subscriber that intercepts the event and modifies it. In this example, it removes a product roadmap entry from the menu and adds a help menu tab with links to product videos. The tab is displayed in a production environment only. ```php 'onHelpMenuConfigure', ]; } public function onHelpMenuConfigure(ConfigureMenuEvent $event): void { $menu = $event->getMenu(); // Remove roadmap menu item if ($menu->getChild('help__general')) { $generalSection = $menu->getChild('help__general'); if ($generalSection->getChild('help__product_roadmap')) { $generalSection->removeChild('help__product_roadmap'); } } // Add videos tab, shown only in production if ($this->kernelDebug === false) { $resourcesSection = $menu->addChild('help__videos', [ 'label' => 'Product videos', ]); $resourcesSection->addChild('help__webinar_v5', [ 'label' => 'Webinar: Introducing Ibexa DXP v5', 'uri' => 'https://www.youtube.com/watch?v=qWaBHG2LRm8', 'extras' => [ 'isHighlighted' => false, 'icon' => 'https://doc.ibexa.co/en/6.0/templating/twig_function_reference/img/icons/video.svg.png', 'description' => 'Discover new features and improvements brought by Ibexa DXP v5.', ], ]); } } } ``` > **Tip: Tip** > > If `autoconfigure` is enabled, the event subscriber is registered as a service by default. If not, register it as a service and tag with `kernel.event.subscriber`. > > ```yaml > services: > App\EventSubscriber\HelpMenuSubscriber: > arguments: > $kernelDebug: '%kernel.debug%' > tags: > - { name: kernel.event_subscriber } > ``` For more ideas on how you can extend the help menu, see [Back office menus](https://doc.ibexa.co/en/saas/administration/back_office/back_office_menus/back_office_menus/index.md). # Product tour > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). Product tours provide interactive guided walkthroughs to help users learn Cohesivo features. Editions: LTS Update Product tour is an in-app onboarding tool that helps back office contributors discover Cohesivo features through interactive, step-by-step guided walkthroughs. Unlike static documentation, product tours provide real-time, contextual guidance directly within the application interface. With product tours, you can create customized onboarding journeys tailored to specific client implementations, user roles, or business processes. This accelerates user adoption, reduces training time, and helps users confidently navigate the platform. Product tour functionality is available from versions 4.6.29 and 5.0.7 as part of the Integrated help package. To use product tours, you must first [install the Integrated help LTS Update](https://doc.ibexa.co/en/saas/administration/back_office/integrated_help/#install-package). ## Key concepts Product tour consists of three main elements: - **Scenario** - a complete onboarding scenario containing multiple steps that guide users through a specific feature or workflow - **Step** - an individual instruction or explanation within a scenario, containing blocks, displayed as an overlay or tooltip - **Block** - a content element within a step, such as text, images, videos, or links that provide information to the user ## Scenario types Cohesivo supports two types of scenarios, each designed for different use cases: ### General scenarios General tours display information in centered modals without targeting specific UI elements. These tours provide an overview of features or concepts and do not require interaction with particular interface elements. General tours are ideal for: - Introducing new users to the platform - Explaining high-level concepts or feature overviews - Welcoming users with customizable background images and branding ![General scenario type](https://doc.ibexa.co/en/saas/administration/back_office/img/product_tour/general_scenario.png "General scenario type") ### Targetable scenarios Targetable scenarios highlight specific UI elements on the page and guide users through interactive workflows. Each step targets a particular element by using a CSS selector, and can draw attention to buttons, navigation elements, or other interface components. Targetable scenarios are ideal for: - Demonstrating specific features or workflows - Guiding users through multi-step processes - Teaching users how to interact with particular UI elements The steps building the scenario support three interaction modes: - **Standard** - Users navigate between steps by clicking **Previous** and **Next** buttons - **Clickable** - Users must click the highlighted element to proceed to the next step - **Draggable** - Users must drag and drop an element to continue the scenario ![Targetable scenario type](https://doc.ibexa.co/en/saas/administration/back_office/img/product_tour/targetable_scenario.png "Targetable scenario type") ## Scenario lifecycle Depending on scenario configuration, they automatically appear to users when they first log in or visit a specific page. Each scenario appears only once for each user. Users can complete a tour with one of the following actions: - by finishing all steps - by skipping it with the **Skip** button in general tours and **Exit tour** in targetable tours - by skipping it with the **Escape** key For **Standard** scenario steps, users can move freely between the previous and next steps. For **Clickable** and **Draggable** steps, users can't go back to the previous step without restarting the scenario and starting from the beginning. At any time, users can manually restart completed tours from their [user settings](https://doc.ibexa.co/projects/userguide/en/6.0/getting_started/get_started/#user-settings). To start building your custom onboarding scenarios, see [Configure product tour](https://doc.ibexa.co/en/saas/administration/back_office/configure_product_tour/index.md). # Configure product tour scenarios > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). Configure custom product tour scenarios with steps, blocks, and interaction modes. Editions: LTS Update You can configure the product tour scenarios to adapt it to your project needs, covering different onboarding scenarios. Product tour scenarios are configured with YAML configuration files. Configuration is SiteAccess-aware, allowing you to create separate onboarding experiences for different back offices in [multisite setups](https://doc.ibexa.co/en/saas/multisite/multisite/index.md). For more advanced customization cases that require PHP code, see [Customize product tour](https://doc.ibexa.co/en/saas/administration/back_office/customize_product_tour/index.md). Use the default provided configuration, available in `config/packages/ibexa_integrated_help_tours.yaml`, as a starting point that you can adjust to your needs. ## Configuration structure You configure product tour scenarios under the `ibexa.system..product_tour` key. Each scenario has a unique identifier and contains steps, which in turn contain blocks. The basic configuration structure of a scenario is as follows: ```yaml ibexa: system: >: # For example, admin or admin_group product_tour: : type: scenario_title_translation_key: # Optional user_groups_excluded: [, ...] # Optional steps: : # Scenario step, unique within a scenario step_title_translation_key: background_image: # Only for general type, optional target: # Only for targetable type, required interaction_mode: # Only for targetable type, optional blocks: - type: params: # Block-specific parameters # ... ``` The product tour scenarios are meant to be translatable. Ibexa recommends using translation keys instead of literal values in the YAML configuration, and providing the translations separately. Use the `ibexa_integrated_help` translation domain. For all the examples below, you can provide the translations by creating a `translations/ibexa_integrated_help.en.yaml` file with the following content: ```yaml tour.my_general_scenario.title: "My general scenario" title: "Welcome!" subtitle: "This is the subtitle" tour.step.description: "This is the description of the step, you can use it to explain what to do in this step." tour.link.documentation: "Documentation link" tour.list.title: "This is the list title" tour.list.item1: "First item" tour.list.item2: "Second item" tour.list.item3: "Third item" ``` To insert a line break into a translation, HTML encode the `
    ` entities to `<br/>`. ## Scenario configuration Each scenario must specify its type and can optionally restrict access by user groups. ### Scenario display order The order of scenarios in the configuration file determines the order in which they are evaluated and, if the right conditions are met, displayed. There are two [scenario types](https://doc.ibexa.co/en/saas/administration/back_office/product_tour/#scenario-types): - `general` scenarios appear at the earliest opportunity (on any page after logging in), with an exception of the user settings area - `targetable` scenarios begin if their `target` element is found in the DOM when the page is loaded. Targetable scenarios don't trigger in the user settings area as well. To control where a targetable tour appears, ensure that the first step targets an element unique to that specific page. You can target elements that appear after a user action, for example, modals like [content browser](https://doc.ibexa.co/en/saas/administration/back_office/browser/browser/index.md), but the first step's target must be present in the DOM when the page is loaded. Once a scenario ends, the system evaluates the next scenario from the configuration and, if applicable, displays it. ### Scenario title Use the optional `scenario_title_translation_key` field to provide a human-readable label for a scenario. This label is displayed in the user settings page where users can reset their product tour progress. ```yaml product_tour: welcome_tour: type: general scenario_title_translation_key: tour.welcome_tour.title ``` If the translation key is not set, the raw scenario identifier is used as the label. Translations must be provided in the `ibexa_integrated_help` translation domain, for example, in `translations/ibexa_integrated_help.en.yaml`. ### User group restrictions Restrict scenario visibility by excluding specific user groups by using their content remote IDs: ```yaml product_tour: my_scenario: user_groups_excluded: ['user_group_content_remote_id_1', 'user_group_content_remote_id_2'] # Exclude specific user groups ``` When creating new [back office user groups](https://doc.ibexa.co/en/saas/users/user_registration/#user-types), decide whether the existing product tour scenarios should be available for these new user groups. If not, add the new group to the exclusion list. > **Caution: Caution** > > If a scenario contains information meant only for specific group of users, always use the `user_groups_excluded` setting to exclude other groups. Don't rely only on UI access restrictions to control the access to scenarios, as a malicious internal user could trigger and preview them outside of the intended place. ## Step configuration Steps define individual instructions within a scenario. The configuration differs based on scenario type: ### General scenario steps General scenario steps display centered modals and support the `background_image` setting, allowing you to set a shared background image for each step. For the background, you can use an absolute URL or place your image in the `public` directory and provide the path relative to it. To resolve the path relative to the site root, [prefix it with `/`](https://developer.mozilla.org/en-US/docs/Web/API/URL_API/Resolving_relative_references#root_relative). ```yaml ibexa: system: admin_group: product_tour: my_general_scenario: type: 'general' scenario_title_translation_key: tour.my_general_scenario.title steps: welcome_step: step_title_translation_key: title background_image: /public/img/background.jpg blocks: - type: title params: ``` ### Targetable tour steps Targetable tour steps highlight specific UI elements by using CSS selectors. You can select a specific element by using the `target` setting. ```yaml ibexa: system: admin_group: product_tour: targetable_dashboard_scenario: type: 'targetable' scenario_title_translation_key: tour.targetable_dashboard_scenario.title steps: dashboard_options: step_title_translation_key: Open Dashboard options target: ".ibexa-db-header__more" # No interaction_mode specified or the value is set to null blocks: - type: text params: ``` If a step's target element doesn't exist on the page, the step isn't displayed and the scenario is stopped. Ensure your configuration matches the actual DOM structure to avoid broken scenarios. Use unique selectors to avoid triggering your scenarios on other pages. #### Interaction modes Select how the scenario step interacts with the target element by using the `interaction_mode` setting. Targetable steps support [three interaction modes](https://doc.ibexa.co/en/saas/administration/back_office/product_tour/#targetable-scenarios): > **Note: Note** > > Clickable and draggable modes are designed for single actions only (buttons, links). You can't select an entire form. If the interaction with the highlighted element results in redirection to a new page or opening a modal window where the previous target element can't be found, the "Previous" navigation button won't be displayed. **Standard mode**: The default value. A tooltip attached to a specific element on the page is displayed. Users continue the scenario with **Previous**/**Next** buttons: ```yaml dashboard_options: step_title_translation_key: Open Dashboard options target: ".ibexa-db-header__more" # No interaction_mode specified or the value is set to null blocks: - type: text params: text_translation_key: Learn how to customize the blocks displayed on your dashboard ``` ![Standard interaction mode](https://doc.ibexa.co/en/saas/administration/back_office/img/product_tour/standard_mode.png "Standard interaction mode") **Clickable mode**: A tooltip attached to a specific element on the page is displayed. Users continue the scenario by clicking the highlighted element. ```yaml open_dashboard_options: step_title_translation_key: Open Dashboard options target: '.ibexa-db-header__more' interaction_mode: clickable blocks: - type: text params: text_translation_key: Click here to customize your dashboard ``` ![Clickable interaction mode](https://doc.ibexa.co/en/saas/administration/back_office/img/product_tour/clickable_mode.png "Clickable interaction mode") **Draggable mode**: A tooltip attached to a specific element on the page is displayed. Users continue the scenario by [dragging](https://developer.mozilla.org/en-US/docs/Web/API/HTML_Drag_and_Drop_API#draggable_items) the highlighted element. ```yaml drag_and_drop_step: step_title_translation_key: Drag-and-drop blocks target: ".c-pb-toolbox-blocks-group__blocks > * .c-pb-toolbox-block__content:first-of-type" interaction_mode: draggable blocks: - type: text params: text_translation_key: Drag-and-drop blocks from the sidebar to the dashboard to customize it ``` You can use this mode only with HTML elements that have the [`draggable` attribute](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Global_attributes/draggable) set to `true`. ![Draggable interaction mode](https://doc.ibexa.co/en/saas/administration/back_office/img/product_tour/draggable_mode.png "Draggable interaction mode") ## Block types Blocks are content elements that make up each step, available both for `general` and `targetable` scenarios. Seven block types are available for building step content, and a scenario step must contain at least one. If multiple blocks are defined for a step, they are displayed one after the other. ### Title block Display bold, prominent titles: ```yaml - type: title params: text_translation_key: subtitle ``` ### Text block Display regular text content: ```yaml - type: text params: text_translation_key: tour.step.description ``` ### Link block Add external or internal links: ```yaml - type: link params: url: https://doc.ibexa.co text_translation_key: tour.link.documentation ``` ### List block Create bulleted lists with title: ```yaml - type: list params: title_translation_key: tour.list.title items_translation_keys: - tour.list.item1 - tour.list.item2 - tour.list.item3 ``` The `title_translation_key` property is optional. ### Media blocks To provide data to the media block, provide absolute URLs or place your image or video files in the `public` directory and provide the path relative to it. To resolve the path relative to the site root, [prefix it with `/`](https://developer.mozilla.org/en-US/docs/Web/API/URL_API/Resolving_relative_references#root_relative). #### Image block Embed images inside the step. You can provide alternative text by using the `alt_translation_key` property. Assuming a `public/img/diagram.jpg` image exists, set the configuration value to `/img/diagram.jpg`. ```yaml - type: image params: src: /public/img/diagram.jpg alt_translation_key: tour.image.alt ``` #### Video block Embed video content by using the [`video` HTML element](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/video): ```yaml - type: video params: # 'Big Buck Bunny' licensed under CC 3.0 by the Blender foundation. Hosted by archive.org url: https://archive.org/download/BigBuckBunny_124/Content/big_buck_bunny_720p_surround.mp4 ``` ### Custom Twig template block For advanced content, use custom Twig templates that allows you to fully control the styling of the block: ```yaml - type: twig_template params: template: custom_template.html.twig ``` Create the dedicated template, for example in `templates/custom_template.html.twig`. ```html+twig {% trans_default_domain 'app' %} {{ 'custom_step_description'|trans }} ``` and provide the required translations in `translations/app.en.yaml`: ```yaml custom_step_description: "This is a description coming from a custom template." ``` ## Configuration examples ### Example 1: General welcome tour The following example showcases all the built-in block types for a `general` scenario consisting of a single step. ```yaml ibexa: system: admin_group: product_tour: my_general_scenario: type: 'general' scenario_title_translation_key: tour.my_general_scenario.title steps: welcome_step: step_title_translation_key: title background_image: /public/img/background.jpg blocks: - type: title params: text_translation_key: subtitle - type: text params: text_translation_key: tour.step.description - type: link params: url: https://doc.ibexa.co text_translation_key: tour.link.documentation - type: image params: src: /public/img/diagram.jpg alt_translation_key: tour.image.alt - type: video params: # 'Big Buck Bunny' licensed under CC 3.0 by the Blender foundation. Hosted by archive.org url: https://archive.org/download/BigBuckBunny_124/Content/big_buck_bunny_720p_surround.mp4 - type: list params: title_translation_key: tour.list.title items_translation_keys: - tour.list.item1 - tour.list.item2 - tour.list.item3 - type: twig_template params: template: custom_template.html.twig ``` ### Example 2: Targetable feature tour with interactive steps The following example showcases how the three interaction modes of a `targetable` scenario can be used to build an onboarding tour for the [customizable dashboard](https://doc.ibexa.co/en/saas/administration/dashboard/customize_dashboard/index.md): ```yaml ibexa: system: admin_group: product_tour: targetable_dashboard_scenario: type: 'targetable' scenario_title_translation_key: tour.targetable_dashboard_scenario.title steps: dashboard_options: step_title_translation_key: Open Dashboard options target: ".ibexa-db-header__more" # No interaction_mode specified or the value is set to null blocks: - type: text params: text_translation_key: Learn how to customize the blocks displayed on your dashboard open_dashboard_options: step_title_translation_key: Open Dashboard options target: '.ibexa-db-header__more' interaction_mode: clickable blocks: - type: text params: text_translation_key: Click here to customize your dashboard customize_dashboard: step_title_translation_key: Customize Dashboard target: '.ibexa-db-actions-popup-menu' interaction_mode: clickable blocks: - type: text params: text_translation_key: Choose "Customize dashboard" drag_and_drop_step: step_title_translation_key: Drag-and-drop blocks target: ".c-pb-toolbox-blocks-group__blocks > * .c-pb-toolbox-block__content:first-of-type" interaction_mode: draggable blocks: - type: text params: text_translation_key: Drag-and-drop blocks from the sidebar to the dashboard to customize it ``` To learn how to customize your scenarios even further with PHP code, see [Customize product tour](https://doc.ibexa.co/en/saas/administration/back_office/customize_product_tour/index.md). # Customize scenarios with PHP code > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). Customize product tour scenarios with custom event listeners Editions: LTS Update You can customize the product tour scenarios with the [`RenderProductTourScenarioEvent`](https://doc.ibexa.co/en/saas/api/event_reference/integrated_help_events/index.md) event. This event is dispatched before a product tour scenario is rendered. You can use it to: - modify tour steps based on user permissions or roles - add or remove steps dynamically - change block content based on runtime conditions - integrate custom data into tour scenarios With the following example, a custom onboarding scenario is built. It starts only when the current user has a pending [notification](https://doc.ibexa.co/projects/userguide/en/6.0/getting_started/notifications/). First, define a custom product tour scenario. It contains a placeholder step with a single block. ```yaml ibexa: system: admin_group: product_tour: notifications: type: 'targetable' steps: placeholder_step: step_title_translation_key: 'This is a placeholder step' target: '.ibexa-header-user-menu__notifications-toggler' blocks: - type: text params: text_translation_key: 'This is a placeholder block, modified during event subscriber execution' ``` Then, create a subscriber that modifies the scenario. ```php ['onRenderScenario'], ]; } public function onRenderScenario(RenderProductTourScenarioEvent $event): void { $scenario = $event->getScenario(); $steps = $scenario->getSteps(); if ($scenario->getIdentifier() !== 'notifications') { return; } foreach ($steps as $step) { $scenario->removeStep($step); } if (!$this->hasUnreadNotifications()) { return; } $customStep = new ProductTourStep(); $customStep->setIdentifier('custom_step_identifier'); $customStep->setInteractionMode('clickable'); $customStep->setTarget('.ibexa-header-user-menu__notifications-toggler'); $customStep->setTitle('You have unread notifications'); $customStep->addBlock(new TextBlock('Click here to preview your unread notifications.')); $customStep->addBlock(new LinkBlock( 'https://doc.ibexa.co/projects/userguide/en/latest/getting_started/notifications/', 'Learn more about notifications' )); $scenario->addStep($customStep); } private function hasUnreadNotifications(): bool { return $this->notificationService->getPendingNotificationCount() > 0; } } ``` The subscriber executes the following actions: - makes sure the correct scenario is being processed - removes all the existing scenario steps - verifies that the current user has a pending notification - adds a custom clickable step to highlight the unread notification ![Scenario built with PHP triggered on unread notification](https://doc.ibexa.co/en/saas/administration/back_office/img/product_tour/custom_scenario.png "Scenario built with PHP triggered on unread notification") # Customize search suggestion > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). Customize search suggestion configuration and sources. In the back office, when you start typing in the search field on the top bar, suggestions about what you could be looking for show up directly under the field. For more information about using this feature to search for content, see [User Documentation](https://doc.ibexa.co/projects/userguide/en/6.0/search/search_for_content/). ## Configuration By default, suggestions start showing up after the user types in at least 3 characters, and 5 suggestions are presented. This can be changed with the following [scoped](https://doc.ibexa.co/en/saas/multisite/multisite_configuration/#scope) configuration: ```yaml ibexa: system: : search: suggestion: min_query_length: 3 result_limit: 5 ``` ## Add custom suggestion source You can add a suggestion source by listening or subscribing to `Ibexa\Contracts\Search\Event\BuildSuggestionCollectionEvent`. During this event, you can add, remove, or replace suggestions by updating its `SuggestionCollection`. After this event, the suggestion collection is sorted by score and truncated to a number of items set in [`result_limit`](#configuration). > **Tip: Tip** > > You can list listeners and subscribers with the following command: > > ```bash > php bin/console debug:event BuildSuggestionCollectionEvent > ``` The following example is boosting product suggestions. It's a subscriber that passes after the default one (because priority is set to zero), adds matching products at a score above the earlier content suggestions, and avoids duplicates. - If the suggestion source finds a number of matching products that is equal or greater than the `result_limit`, only those products end up in the suggestion. - If it finds less than `result_limit` products, those products are on top of the suggestion, followed by items from another suggestion source until the limit is met. - If it doesn't find any matching products, only items from the default suggestion source are shown. This example event subscriber is implemented in the `src/EventSubscriber/MySuggestionEventSubscriber.php` file. It uses [`ProductService::findProducts`](https://doc.ibexa.co/en/saas/product_catalog/product_api/#products), and returns the received event after having manipulated the `SuggestionCollection`: ```php ['onBuildSuggestionCollectionEvent', -1], ]; } public function onBuildSuggestionCollectionEvent(BuildSuggestionCollectionEvent $event): BuildSuggestionCollectionEvent { $suggestionQuery = $event->getQuery(); $suggestionCollection = $event->getSuggestionCollection(); $text = $suggestionQuery->getQuery(); $words = explode(' ', (string) preg_replace('/\s+/', ' ', $text)); $limit = $suggestionQuery->getLimit(); try { $productQuery = new ProductQuery(null, new Criterion\LogicalOr([ new Criterion\ProductName(implode(' ', array_map(static fn (string $word): string => "$word*", $words))), new Criterion\ProductCode($words), new Criterion\ProductType($words), ]), [], 0, $limit); $searchResult = $this->productService->findProducts($productQuery); if ($searchResult->getTotalCount()) { $maxScore = 0.0; $suggestionsByContentIds = []; /** @var \Ibexa\Contracts\Search\Model\Suggestion\ContentSuggestion $suggestion */ foreach ($suggestionCollection as $suggestion) { $maxScore = max($suggestion->getScore(), $maxScore); $suggestionsByContentIds[$suggestion->getContent()->id] = $suggestion; } /** @var \Ibexa\ProductCatalog\Local\Repository\Values\Product $product */ foreach ($searchResult as $product) { $contentId = $product->getContent()->id; if (array_key_exists($contentId, $suggestionsByContentIds)) { $suggestionCollection->remove($suggestionsByContentIds[$contentId]); } $productSuggestion = new ProductSuggestion($maxScore + 1, $product); $suggestionCollection->append($productSuggestion); } } } catch (\Throwable $throwable) { $this->logger->error($throwable); } return $event; } } ``` To have the logger injected thanks to the `LoggerAwareTrait`, this subscriber must be registered as a service: ```yaml services: #… App\EventSubscriber\MySuggestionEventSubscriber: ~ ``` To represent the product suggestion data, a `ProductSuggestion` class is created in `src/Search/Model/Suggestion/ProductSuggestion.php`: ```php getName()); $this->product = $product; } public function getProduct(): Product { return $this->product; } } ``` This representation needs a normalizer to be transformed into a JSON. `ProductSuggestionNormalizer::supportsNormalization` returns that this normalizer supports `ProductSuggestion`. `ProductSuggestionNormalizer::normalize` returns an array of scalar values which can be transformed into a JSON object. Alongside data about the product, this array must have a `type` key, whose value is used later for rendering as an identifier. In `src/Search/Serializer/Normalizer/Suggestion/ProductSuggestionNormalizer.php`: ```php */ public function normalize($object, ?string $format = null, array $context = []): array { /** @var \App\Search\Model\Suggestion\ProductSuggestion $object */ return [ 'type' => 'product', 'name' => $object->getName(), 'productCode' => $object->getProduct()->getCode(), 'productTypeIdentifier' => $object->getProduct()->getProductType()->getIdentifier(), 'productTypeName' => $object->getProduct()->getProductType()->getName(), ]; } public function supportsNormalization($data, ?string $format = null, array $context = []): bool { return $data instanceof ProductSuggestion; } public function getSupportedTypes(?string $format): array { return [ ProductSuggestion::class => true, ]; } } ``` This normalizer is added to suggestion normalizers by decorating `ibexa.search.suggestion.serializer` and redefining its list of normalizers: ```yaml services: #… App\Search\Serializer\Normalizer\Suggestion\ProductSuggestionNormalizer: autoconfigure: false app.search.suggestion.serializer: decorates: ibexa.search.suggestion.serializer class: Symfony\Component\Serializer\Serializer autoconfigure: false arguments: $normalizers: - '@App\Search\Serializer\Normalizer\Suggestion\ProductSuggestionNormalizer' - '@Ibexa\Search\Serializer\Normalizer\Suggestion\ContentSuggestionNormalizer' - '@Ibexa\Search\Serializer\Normalizer\Suggestion\LocationNormalizer' - '@Ibexa\Search\Serializer\Normalizer\Suggestion\ParentLocationCollectionNormalizer' - '@Ibexa\Search\Serializer\Normalizer\Suggestion\SuggestionCollectionNormalizer' $encoders: - '@serializer.encoder.json' ``` > **Tip: Tip** > > At this point, it's possible to test the suggestion JSON. The route is `/suggestion` with a GET parameter `query` for the searched text. > > For example, log in to the back office to have a session cookie, then access the route through the back office SiteAccess, such as `/admin/suggestion?query=platform`. If you have a product with "platform" in its name, it is returned as the first suggestion. A JavaScript renderer displays the normalized product suggestion. This renderer is wrapped in an immediately executed function. This wrapping function must define a rendering function and register it as a renderer. It's registered as `autocomplete.renderers.` by using the type identifier defined in the normalizer. ```javascript (function (global, doc, ibexa, Routing) { const renderItem = (result, searchText) => { // Compute suggestion item's HTML return html; } ibexa.addConfig('autocomplete.renderers.', renderItem, true); })(window, document, window.ibexa, window.Routing); ``` To fit into the back office design, you can take HTML structure and CSS class names from an existing suggestion template `vendor/ibexa/admin-ui/src/bundle/Resources/views/themes/admin/ui/global_search_autocomplete_content_item.html.twig`. To allow template override and ease HTML writing, the example is also loading a template to render the HTML. Here is a complete `assets/js/admin.search.autocomplete.product.js` from the product suggestion example: ```js (function (global, doc, ibexa, Routing) { const renderItem = (result, searchText) => { const globalSearch = doc.querySelector('.ibexa-global-search'); const { highlightText } = ibexa.helpers.highlight; const autocompleteHighlightTemplate = globalSearch.querySelector('.ibexa-global-search__autocomplete-list').dataset .templateHighlight; const { getContentTypeIconUrl, getContentTypeName } = ibexa.helpers.contentType; const autocompleteItemTemplate = globalSearch.querySelector('.ibexa-global-search__autocomplete-product-template').dataset .templateItem; return autocompleteItemTemplate .replace('{{ productHref }}', Routing.generate('ibexa.product_catalog.product.view', { productCode: result.productCode })) .replace('{{ productName }}', highlightText(searchText, result.name, autocompleteHighlightTemplate)) .replace('{{ productCode }}', result.productCode) .replace('{{ productTypeIconHref }}', getContentTypeIconUrl(result.productTypeIdentifier)) .replace('{{ productTypeName }}', result.productTypeName); }; ibexa.addConfig('autocomplete.renderers.product', renderItem, true); })(window, document, window.ibexa, window.Routing); ``` To be loaded in the back office layout, this file must be added to Webpack entry `ibexa-admin-ui-layout-js`. At the end of `webpack.config.js`, add it by using `ibexaConfigManager`: ```javascript //… const ibexaConfigManager = require('./ibexa.webpack.config.manager.js'); ibexaConfigManager.add({ ibexaConfig, entryName: 'ibexa-admin-ui-layout-js', newItems: [path.resolve(__dirname, './assets/js/admin.search.autocomplete.product.js')], }); ``` The renderer, `renderItem` function from `admin.search.autocomplete.product.js`, loads an HTML template from a wrapping DOM node [dataset](https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/dataset). This wrapping node exists only once and the renderer loads the template several times. The example template for this wrapping node is stored in `templates/themes/admin/ui/global_search_autocomplete_product_template.html.twig` (notice the CSS class name used by the renderer to reach it): ```html+twig
    ``` - At HTML level, it wraps the product item template in its dataset attribute `data-template-item`. - At Twig level, it includes the item template, replaces Twig variables with the strings used by the JS renderer, and passes it to the [`escape` filter](https://twig.symfony.com/doc/3.x/filters/escape.html) with the HTML attribute strategy. To be present, this wrapping node template must be added to the `admin-ui-global-search-autocomplete-templates` group of tabs components: ```yaml services: #… ibexa.search.autocomplete.product_template: parent: Ibexa\AdminUi\Component\TabsComponent arguments: $template: '@@ibexadesign/ui/global_search_autocomplete_product_template.html.twig' $groupIdentifier: 'global-search-autocomplete-product' tags: - { name: ibexa.twig.component, group: global-search-autocomplete-templates } ``` The template for the product suggestion item follows, named `templates/themes/admin/ui/global_search_autocomplete_product_item.html.twig`: ```html+twig
  • {{ product_name }}
    {{ product_code }}
    {{ product_type_name }}
  • ``` ## Replace default suggestion source To replace the default suggestion source, [decorate](https://symfony.com/doc/7.4/service_container/decoration.html) the built-in `ContentSuggestionSubscriber` subscriber with your own: ```yaml services: #… App\EventSubscriber\MySuggestionEventSubscriber: decorates: Ibexa\Search\EventDispatcher\EventListener\ContentSuggestionSubscriber ``` # Customize search sorting > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). Add a "sort by" method to the back office search result page. You can customize the **Sort by** menu in the back office search result page, for example, by adding new sort criteria. To do it, you must create a service that implements the `Ibexa\Contracts\Search\SortingDefinition\SortingDefinitionProviderInterface` and tag it with `ibexa.search.sorting_definition.provider`. The following example class implements `SortingDefinitionProviderInterface::getSortingDefinitions`, and adds two definitions to sort by section name. A sorting definition contains an identifier, a menu label, a list of content search Sort Clauses, which could be either [default](https://doc.ibexa.co/en/saas/search/sort_clause_reference/sort_clause_reference/#sort-clauses) or [custom](https://doc.ibexa.co/en/saas/search/extensibility/create_custom_sort_clause/index.md), and a priority value to position them in the menu. It also implements `TranslationContainerInterface::getTranslationMessages` to provide two default English translations in the `ibexa_search` namespace. Create the `src/Search/SortingDefinition/Provider/SectionNameSortingDefinitionProvider.php` file: ```php translator->trans('sort_definition.section_name_asc.label'), [ new SortClause\SectionName(Query::SORT_ASC), ], 333 ), new SortingDefinition( 'section_desc', $this->translator->trans('sort_definition.section_name_desc.label'), [ new SortClause\SectionName(Query::SORT_DESC), ], 369 ), ]; } public static function getTranslationMessages(): array { return [ (new Message('sort_definition.section_name_asc.label'))->setDesc('Sort by section A-Z'), (new Message('sort_definition.section_name_desc.label'))->setDesc('Sort by section Z-A'), ]; } } ``` Then add a service definition to `config/services.yaml`: ```yaml services: #… App\Search\SortingDefinition\Provider\SectionNameSortingDefinitionProvider: tags: - name: ibexa.search.sorting_definition.provider ``` You can extract a translation file with the `jms:translation:extract` command, for example, `php bin/console jms:translation:extract en --dir=src --output-dir=translations` to obtain the `translations/ibexa_search.en.xlf` file. You could also create it manually, as `translations/messages.en.yaml` file with the following contents: ```yaml sort_definition.section_name_asc.label: 'Sort by section A-Z' sort_definition.section_name_desc.label: 'Sort by section Z-A' ``` # Recent activity > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). Log and monitor activity through UI, PHP API and REST API. Editions: Experience Recent activity log displays last actions in the repository (whatever their origin is, for example, back office, REST, migration, CLI, or CRON). ![Recent activity](https://doc.ibexa.co/en/saas/administration/img/admin_panel_recent_activity.png) To learn more about its back office usage and the actions logged by default, see [Recent activity in User Documentation](https://doc.ibexa.co/projects/userguide/en/6.0/recent_activity/recent_activity/). ## Configuration With some configuration, you can customize the log length in the database or on screen, or disable the logging completely. A command maintains the log size in database, it should be scheduled through CRON. ### Log retention The `ibexa.repositories..activity_log.truncate_after_days` setting sets the number of days a log entry is kept before it's deleted by the `ibexa:activity-log:truncate` command (default value: 30 days). For example, the following configuration sets 15 days of life to the log entries on the `default` repository: ```yaml ibexa: repositories: default: activity_log: truncate_after_days: 15 ``` To automate a regular truncation, you must schedule the command `ibexa:activity-log:truncate`. To minimize the number of entries to delete, it's recommended that you execute the command more than one time a day. ### Display limit The `ibexa.system..activity_log.pagination.activity_logs_limit` setting sets the number of log items shown per page in the back office (default value: 25). For example, the following configuration sets 20 context groups per page for the `admin_group` SiteAccess group: ```yaml ibexa: system: admin_group: activity_log: pagination: activity_logs_limit: 20 ``` A log item is a group of entries, or an entry without group. ### Disable activity log The `ibexa.repositories..activity_log.enabled` setting can disable activity log entirely for a given [repository](https://doc.ibexa.co/en/saas/administration/configuration/repository_configuration/index.md). For example, to disable the activity log for the `default` repository: ```yaml ibexa: repositories: default: activity_log: enabled: false ``` You can also disable activity log for a single action by using the [PHP API](#disable-logging-activities). ## Permission and security The [`activity_log/read`](https://doc.ibexa.co/en/saas/permissions/policies/#activity-log) policy gives a role the access to the **Admin** -> **Activity list**, the dashboard's **Recent activity** block, and the user profile's **Recent activity**. It can be limited to "Only own logs" ([`ActivityLogOwner`](https://doc.ibexa.co/en/saas/permissions/limitation_reference/#activity-log-owner-limitation)). The policy should be given to every roles having access to the back office, at least with the `ActivityLogOwner` owner limitation, to allow them to use the "Recent activity" block in the [default dashboard](https://doc.ibexa.co/en/saas/administration/dashboard/configure_default_dashboard/index.md) or their [custom dashboard](https://doc.ibexa.co/en/saas/administration/dashboard/customize_dashboard/index.md). This policy is required to view [activity log in user profile](https://doc.ibexa.co/projects/userguide/en/6.0/getting_started/get_started/#view-and-edit-user-profile), if the user profile is enabled. > **Caution: Caution** > > Don't assign `activity_log/read` permission to the Anonymous role, even with the owner limitation, because this role is shared among all unauthenticated users. ## User privacy > **Caution: Caution** > > A username of the User who performs the action is logged. When acting through the web server, the User's IP address is also logged. Other access, such as console commands, doesn't log an IP. Your Data Protection Officer or GDPR representative should be aware of this, so they can ensure users are informed if needed, depending on your use case, jurisdiction, and company policy. > > For example, if a content edition feature, such as reader's comments, is available in the front office, the recent activity log records the front users' IPs. ## PHP API The `ActivityLogService` PHP API can be used to browse activity logs and write new entries. ### Searching in the Activity Log groups You can search among the activity log entry groups with the `ActivityLogService::findGroups` method, by passing an `Ibexa\Contracts\ActivityLog\Values\ActivityLog\Query` object. This `Query`'s constructor has four arguments: - `$criteria` - an array of criteria from `Ibexa\Contracts\ActivityLog\Values\ActivityLog\Criterion` combined as a logical AND. - `$sortClauses` - an array of `Ibexa\Contracts\ActivityLog\Values\ActivityLog\SortClause`. - `$offset` - a zero-based index integer indicating at which group to start, its default value is `0` (zero, nothing skipped). - `$limit` - an integer as the maximum returned group count, default is 25. See [Activity Log Search Criteria reference](https://doc.ibexa.co/en/saas/search/activity_log_search_reference/activity_log_criteria/index.md) and [Activity Log Search Sort Clauses reference](https://doc.ibexa.co/en/saas/search/activity_log_search_reference/activity_log_sort_clauses/index.md) to discover query possibilities. In the following example, log groups that contain at least one creation of a Content item are displayed in terminal, with a maximum of 10 groups within the last hour. It uses the default `admin` user that has a [permission](#permission-and-security) to list everyone's entries. ```php permissionResolver->setCurrentUserReference($this->userService->loadUserByLogin('admin')); foreach ($this->activityLogService->findGroups($query) as $activityLogGroup) { if ($activityLogGroup->getSource()) { $io->section($activityLogGroup->getSource()->getName()); } if ($activityLogGroup->getDescription()) { $io->text($activityLogGroup->getDescription()); } $table = []; foreach ($activityLogGroup->getActivityLogs() as $activityLog) { $name = "“{$activityLog->getObjectName()}”"; $content = $activityLog->getRelatedObject(); if ($content && method_exists($content, 'getName') && $content->getName() !== $activityLog->getObjectName()) { $name = "“{$content->getName()}” (formerly “{$activityLog->getObjectName()}”)"; } $table[] = [ $activityLogGroup->getLoggedAt()->format(\DateTime::ATOM), $activityLog->getObjectId(), $name, $activityLog->getAction(), $activityLogGroup->getUser() ? $activityLogGroup->getUser()->login : '', $activityLogGroup->getIp() ? $activityLogGroup->getIp()->getIp() : '', ]; } $io->table([ 'Logged at', 'Obj. ID', 'Object Name', 'Action', 'User', 'IP', ], $table); } return Command::SUCCESS; } } ``` ```console % php bin/console app:monitor-content-creation web --- --------------------------- --------- --------------------------- -------- ---------- ------------ Logged at Obj. ID Object Name Action User IP --------------------------- --------- --------------------------- -------- ---------- ------------ 2024-01-29T15:01:57+00:00 323 “Bar” (formerly “Folder”) create jane_doe 172.20.0.5 --------------------------- --------- --------------------------- -------- ---------- ------------ migration --------- Migrating file: create_foo_company --------------------------- --------- -------------------- -------------- ------- ---- Logged at Obj. ID Object Name Action User IP --------------------------- --------- -------------------- -------------- ------- ---- 2024-01-29T14:58:53+00:00 317 “Foo Company Ltd.“ create admin 2024-01-29T14:58:53+00:00 317 “Foo Company Ltd.“ publish admin 2024-01-29T14:58:53+00:00 318 “Members“ create admin 2024-01-29T14:58:53+00:00 318 “Members“ publish admin 2024-01-29T14:58:53+00:00 317 “Foo Company Ltd.“ create_draft admin 2024-01-29T14:58:53+00:00 317 “Foo Company Ltd.“ update admin 2024-01-29T14:58:53+00:00 317 “Foo Company Ltd.“ publish admin 2024-01-29T14:58:53+00:00 319 “Address Book“ create admin 2024-01-29T14:58:53+00:00 319 “Address Book“ publish admin 2024-01-29T14:58:53+00:00 317 “Foo Company Ltd.“ create_draft admin 2024-01-29T14:58:53+00:00 317 “Foo Company Ltd.“ update admin 2024-01-29T14:58:53+00:00 317 “Foo Company Ltd.“ publish admin 2024-01-29T14:58:53+00:00 320 “HQ“ create admin 2024-01-29T14:58:53+00:00 320 “HQ“ publish admin 2024-01-29T14:58:53+00:00 317 “Foo Company Ltd.“ create_draft admin 2024-01-29T14:58:53+00:00 317 “Foo Company Ltd.“ update admin 2024-01-29T14:58:53+00:00 317 “Foo Company Ltd.“ publish admin --------------------------- --------- -------------------- -------------- ------- ---- ``` ### Add custom Activity Log entries > **Caution: Caution** > > Keep activity logging as light as possible. Don't make database requests or heavy computation at logging time. Keep them for activity log list display time. If needed, you can [disable logging for specific operations](#disable-logging-activities) using the PHP API. #### Create an entry Your custom features can write into the activity log. First, inject `Ibexa\Contracts\ActivityLog\ActivityLogServiceInterface` into your PHP class from where you want to log an activity (such as a custom event subscriber, event listener, service, or controller). In the following example, an event subscriber is subscribing to an event dispatched by a custom feature. This event has the information needed by a log entry (see details after the example). ```php 'onMyFeatureEvent', ]; } public function onMyFeatureEvent(MyFeatureEvent $event): void { /** @var \App\MyFeature\MyFeature $object */ $object = $event->getObject(); $className = $object::class; $id = (string)$object->id; $action = $event->getAction(); $activityLog = $this->activityLogService->build($className, $id, $action); $activityLog->setObjectName($object->name); $this->activityLogService->save($activityLog); } } ``` `ActivityLogService::build()` function returns an `Ibexa\Contracts\ActivityLog\Values\CreateActivityLogStruct` which can then be passed to `ActivityLogService::save`. `ActivityLogService::build` has three arguments: - `$className` is a FQCN of the object actually manipulated by the feature, for example `Ibexa\Contracts\Core\Repository\Values\Content\Content::class` - `$id` is an ID or identifier of the manipulated object, for example, the Content ID cast to string - `$action` is an identifier of the performed object manipulation, or example, `create`, `update` or `delete` The returned `CreateActivityLogStruct` is always related to the currently logged-in user. You can still display activity log of an object which was deleted or renamed. To store the name of the log, you need to use `CreateActivityLogStruct::setName` before saving the log entry. This stored name can be used at the time of displaying information whether the associated object isn't available anymore, or to check if it has been renamed. #### Context group If you log several related entries at once, you can group them into a context. Context is a set of actions done for the same purpose, for example, it could group the actions of a CRON that fetches third party data and updates content items. The built-in contexts include: - `web` - groups actions made in the back office, like the update and the publishing of a new content item's version - `migration` - groups every action from a migration file execution A context group counts as one item in regard to `activity_logs_limit` configuration and `ActivityLogService::findGroups`'s `$limit` argument. To open a context group, use `ActivityLogService::prepareContext` which has two arguments: - `$source` - describes, usually through a short identifier, what is triggering the set of actions. For example, some already existing sources are `web` (incl. actions from the back office), `graphql`, `rest` and `migration` - `$description` - an optional, more specific contextualisation. For example, `migration` context source is associated with the migration file name in its context description. To close a context group, use `ActivityLogService::dismissContext`. In the following example, several actions are logged into one context group, even those triggered by a cascade outside the piece of code: - `my_feature` - `init` - `create` - `publish` - `simulate` - `complete` ```php $this->activityLogService->prepareContext('my_feature', 'Operation description'); $activityLogStruct = $this->activityLogService->build(MyFeature::class, $id, 'init'); $activityLogStruct->setObjectName("My Feature #$id"); $this->activityLogService->save($activityLogStruct); $contentCreateStruct = $this->contentService->newContentCreateStruct($this->contentTypeService->loadContentTypeByIdentifier('folder'), 'eng-GB'); $contentCreateStruct->setField('name', "My Feature Folder #$id", 'eng-GB'); $locationCreateStruct = new LocationCreateStruct(['parentLocationId' => 2]); $draft = $this->contentService->createContent($contentCreateStruct, [$locationCreateStruct]); $this->contentService->publishVersion($draft->versionInfo); $event = new MyFeatureEvent(new MyFeature(['id' => $id, 'name' => "My Feature #$id"]), 'simulate'); $this->eventDispatcher->dispatch($event); $activityLogStruct = $this->activityLogService->build(MyFeature::class, $id, 'complete'); $activityLogStruct->setObjectName("My Feature #$id"); $this->activityLogService->save($activityLogStruct); $this->activityLogService->dismissContext(); ``` Context groups can't be nested. If a new context is prepared when a context is already grouping log entries, this new context is ignored. To start a new context, make sure to previously dismiss the existing one. When displayed in the back office, a context group is folded below its first entry. The `my_feature` context from the example is folded below its first action, the `init` action. Other actions are displayed after you click the **Show more** button. ![The example context group displayed on the Recent Activity page](https://doc.ibexa.co/en/saas/administration/img/activity_log_group.png "my_feature context from the example") #### Display log entries To display your log entry, if your object's PHP class isn't already covered, you have to: - implement `ClassNameMapperInterface` to associate the class name with an identifier, - eventually create a `PostActivityListLoadEvent` subscriber if you need to load the object for the template, - create a template to display this class log entries. You can have a template that is: - specific to a class identifier and placed in `templates/themes//activity_log/ui/.html.twig` - specific to an action on an identifier and placed in `templates/themes//activity_log/ui//.html.twig` Template existence is tested in reverse order: if there is no action that specifies the template, the identifier's default is used. For the same identifier, you could have specific templates for few actions, and a default one for the remaining actions. A default template is used if no template is found for the identifier. The built-in default template `@ibexadesign/activity_log/ui/default.html.twig` has an empty `activity_log_description_widget` block and doesn't display anything for unknown objects. Your template can extend `@ibexadesign/activity_log/ui/default.html.twig`, and only redefine the `activity_log_description_widget` block for your objects. First, follow an example of a default template overriding the one from the bundle. It can be used during development as a fallback for classes that aren't mapped yet. ```twig {% extends '@IbexaActivityLog/themes/admin/activity_log/ui/default.html.twig' %} {%- block activity_log_description_widget -%} {{ dump(log) }} {%- endblock activity_log_description_widget -%} ``` Here is an example of a `ClassNameMapperInterface` associating the class `App\MyFeature\MyFeature` with the identifier `my_feature`: ```php 'my_feature'; } public static function getTranslationMessages(): array { return [ (new Message('ibexa.activity_log.search_form.object_class.my_feature', 'ibexa_activity_log')) ->setDesc('My Feature'), ]; } } ``` This mapper also provides a translation for the class name in the **Filters** menu. This translation can be extracted with `php bin/console jms:translation:extract en --domain=ibexa_activity_log --dir=src --output-dir=translations`. To be taken into account, this mapper must be registered as a service: ```yaml services: App\ActivityLog\ClassNameMapper\MyFeatureNameMapper: ~ ``` Here is an example of a `PostActivityListLoadEvent` subscriber which loads the related object when it's an `App\MyFeature\MyFeature`, and attaches it to the log entry: ```php ['loadMyFeature'], ]; } public function loadMyFeature(PostActivityGroupListLoadEvent $event): void { $visitedIds = []; $list = $event->getList(); foreach ($list as $logGroup) { foreach ($logGroup->getActivityLogs() as $log) { if ($log->getObjectClass() !== MyFeature::class) { continue; } $id = (int)$log->getObjectId(); try { if (!array_key_exists($id, $visitedIds)) { $visitedIds[$id] = $this->myFeatureService->load($id); } if ($visitedIds[$id] === null) { continue; } $log->setRelatedObject($visitedIds[$id]); } catch (NotFoundException|UnauthorizedException) { $visitedIds[$id] = null; } } } } } ``` The following template is made to display the object of `App\MyFeature\MyFeature` (now identified as `my_feature`) when the action is `simulate`, so, it's named in `templates/themes/admin/activity_log/ui/my_feature/simulate.html.twig`. Thanks to the previous subscriber, the related object is available at display time: ```twig {% extends '@ibexadesign/activity_log/ui/default.html.twig' %} {%- block activity_log_description_widget -%} {% if log.getRelatedObject() is not null %} {{- log.getRelatedObject().name -}} {% if log.getRelatedObject().name != log.getObjectName() %} (was named “{{ log.getObjectName() }}”) {% endif %} {% else %} {{ log.getObjectName() }} (which doesn't exist anymore) {% endif %} {%- endblock activity_log_description_widget -%} ``` ### Disable logging activities You can disable logging the activities with PHP API, for example, when loading large amounts of data in cases where you don't want logging to slow down the process or the actions to be included in the log. Call [`ActivityLogService::disable()`](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-ActivityLog-ActivityLogServiceInterface.html#method_disable) before running the relevant code, then [`ActivityLogService::enable()`](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-ActivityLog-ActivityLogServiceInterface.html#method_enable) to restore the logging process: ```php disable(); // Perform operations that should not be logged to the activity log // ... $activityLogService->enable(); ``` When disabled, any call to [`ActivityLogService::save()`](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-ActivityLog-ActivityLogServiceInterface.html#method_save) has no effect and no entries are written to the database. You can check the current state with [`ActivityLogService::isEnabled()`](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-ActivityLog-ActivityLogServiceInterface.html#method_isEnabled) and [`ActivityLogService::isDisabled()`](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-ActivityLog-ActivityLogServiceInterface.html#method_isDisabled). ## REST API You can browse activity logs with REST API. For more information, see the [REST API reference](https://doc.ibexa.co/en/saas/api/rest_api/rest_api_reference/rest_api_reference.html#tag/Activity-Log). # Content management # Content management > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). Manage content in Cohesivo by learning about the content model, field types, pages, forms, workflows, and more. - [Content management product guide](https://ez-systems-developer-documentation--3381.com.readthedocs.build/en/3381/content_management/content_management_guide/): Read the content management product guide and learn how to create, modify, and display information to the target audience. - [Content model](https://ez-systems-developer-documentation--3381.com.readthedocs.build/en/3381/content_management/content_model/): Cohesivo's content model relies on content items that are instances of content types and contain content fields. - [Locations](https://ez-systems-developer-documentation--3381.com.readthedocs.build/en/3381/content_management/locations/): Locations hold published content items and can be used to control visibility. - [Field type reference](https://ez-systems-developer-documentation--3381.com.readthedocs.build/en/3381/content_management/field_types/field_type_reference/field_type_reference/): Cohesivo offers a range of built-in field types that cover most common needs when creating content. - [Pages](https://ez-systems-developer-documentation--3381.com.readthedocs.build/en/3381/content_management/pages/pages/): Pages are block-based special types of content that editors can create and modify by using a visual drag-and-drop editor. - [Forms](https://ez-systems-developer-documentation--3381.com.readthedocs.build/en/3381/content_management/forms/forms/): Forms are a type of content item that you can use to improve the functionality of your website. - [Taxonomy](https://ez-systems-developer-documentation--3381.com.readthedocs.build/en/3381/content_management/taxonomy/taxonomy/): A taxonomy uses tags to categorize and organize content - [Workflow](https://ez-systems-developer-documentation--3381.com.readthedocs.build/en/3381/content_management/workflow/workflow/): Workflow controls how content items pass between stages and allows setting up editorial flows, for example for reviews and proofreading. - [Data migration](https://ez-systems-developer-documentation--3381.com.readthedocs.build/en/3381/content_management/data_migration/data_migration/): Data migration enables you to import and export repository data by using YAML files. # Content management product guide > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). Read the content management product guide and learn how to create, modify, and display information to the target audience. ## What is content management The term “content management” covers all the tasks that you need to perform to create, edit and present content to its intended audience. The content management model applied in Cohesivo lies at the foundation of the entire system. A system that relies on roles and permissions controls access to content items and is granular and powerful enough to be used in managing user accounts, corporate accounts, products, or process definitions. ## Availability Content management capabilities are available in all Cohesivo editions. ## How does it work Cohesivo revolves around content management. Many things here are content items, including: - sites - folders - pages - articles or posts - products - forms - media (for example, images or videos) - user accounts You can set up content structure, define the templates to be filled with content, and assign different areas of the structure to your editors. Next steps would be to create the actual content, and then classify content items, and organize them as necessary. You can then publish the content directly, by building a website or a web store, or by using external systems together with a [headless CMS](https://developers.ibexa.co/headless-cms) that relies on the Cohesivo technology. ## Content structure All content in Cohesivo is organized hierarchically, into what is called a [**content tree**](https://doc.ibexa.co/en/saas/administration/back_office/content_tree/index.md). This tree-like structure repeats throughout the system, and applies to content, taxonomies, categories, and the like. Traditional as the structure may look, with relations and multiple location support, a single content item can be referenced by another content item and accessed from different places of the tree, which allows you to build complex architectures with multiple locales and output channels. ![Content structure in a Content Browser](https://doc.ibexa.co/en/saas/content_management/img/content_tree.png) ## Content model A structure of elements that *store* content information is referred to as the **content model**. Cohesivo comes with a predefined content model that includes a broad set of various field types and several content types. You can customize and adapt the content model to your organization's needs and the type of output channel that you use. If need be, development teams can [create new field types](https://doc.ibexa.co/en/saas/tutorials/generic_field_type/creating_a_point2d_field_type/index.md), to enhance editor and visitor experiences. Content managers or even editors can then apply such field types when they modify existing or create new content types. The editing interface lets all users, including those with no coding experience, create or modify certain areas of the content model. For technical details, see [a Content model](https://doc.ibexa.co/en/saas/content_management/content_model/#content-model). ### Field types [Field types](https://doc.ibexa.co/en/saas/content_management/field_types/field_types/index.md) are the smallest elements of the content model’s structure. Cohesivo comes with many built-in field types that cover most common needs, for example, Text line, RichText, Integer, Measurement, or Map location. Their role is to: - store data - validate input data - make the data searchable - display fields of a given field type For a complete list of available field types, see [field type reference](https://doc.ibexa.co/en/saas/content_management/field_types/field_type_reference/field_type_reference/index.md). ![Field types and fields](https://doc.ibexa.co/en/saas/content_management/img/field_types.png) ### Fields Once you use a field type to design and build a content type definition, and define its settings, it becomes a field. Fields can be as simple as Name, based on a Text line field type, or as complex as page, based on a landing page field type, with multiple options to set and choose from: ![Landing page field settings](https://doc.ibexa.co/en/saas/content_management/img/fields.png) ### Content types Life gets easier when you have templates to fill in with content. Content types are such templates, which editors use to create content items. Content types define what fields are available in the content item. Cohesivo comes with several basic content types, and creating new ones, editing, and deleting them is done by using a visual interface, with no coding skills needed. ![Content types vs. content items](https://doc.ibexa.co/en/saas/content_management/img/content_types.png) ### Content items Content items are pieces of content, such as, for example, products, articles, blog posts, or media. In Cohesivo, everything is a content item — not only pages, articles or products, but also all media (for example, images or videos) or even user accounts. Each content item, apart from its name and identifier, contains a composition of fields, which differs depending on the type of content. For example, articles might have for example, a title, an author, a body, and an image, while products may have, for example, a name, category, price, size, or color. ### Forms Forms could be seen as a special kind of content items, because their role is to gather information from website users and not present it. You create them from basic form fields available in Cohesivo. By adding forms to the website, you can increase the website’s functionality and improve user experience. Certain editions of Cohesivo come with a visual [Form Builder](https://doc.ibexa.co/projects/userguide/en/6.0/content_management/work_with_forms/). ## Content management capabilities Each content item has at least one location within the content tree, and can have several versions and multiple translations. It can also have related assets, such as images or other media, and assigned keywords, or tags. You can use these characteristics in combination with system features to create the most comprehensive and functional digital presence for your organization. ### Content characteristics #### Locations When a content item is created and published, it's assigned a place in the content tree, designated by a location ID. A single content item can have more than one location ID, which means that the same content can be found on different branches of the tree. However, a single location can have only one content item assigned to it. ![Locations](https://doc.ibexa.co/en/saas/content_management/img/locations.png) Locations can be used to control the availability of content items to end users: you can [hide specific locations](https://doc.ibexa.co/projects/userguide/en/6.0/content_management/content_organization/manage_locations_urls/#hide-locations) of a content item, while others remain available. By [swapping locations](https://doc.ibexa.co/projects/userguide/en/6.0/content_management/content_organization/manage_locations_urls/#swap-locations), you can immediately replace an obsolete version of a content item with an updated one. #### Versions Content items can have several [versions](https://doc.ibexa.co/projects/userguide/en/6.0/content_management/content_versions/). By default, there are three version statuses available: draft, published, and archived. Before they're published, drafts can be routed between different user roles for review and approval. ![Versions](https://doc.ibexa.co/en/saas/content_management/img/versions.png) Editors can [compare different content item versions](https://doc.ibexa.co/projects/userguide/en/6.0/content_management/workflow_management/work_with_versions/#compare-versions) by using the Compare versions feature. #### Translations Content items can have more than one [translation](https://doc.ibexa.co/projects/userguide/en/6.0/content_management/translate_content/). If a website has different fronts, for different locales, and different language versions of content exist, Cohesivo serves the one that matches the locale. ![Translations](https://doc.ibexa.co/en/saas/content_management/img/translations.png) Editors can compare different translations of the same content items with the Compare versions feature mentioned above. #### Relations A [relation](https://doc.ibexa.co/en/saas/content_management/content_relations/index.md) can exist between any two content items in the content tree. For example, blog posts featured in the website's main page are in a relation with the page that they're embedded in. Or, instead of direct attachments, an article can use images that are separate content items outside the article, and are referenced through a relation. ## Content arrangement In Cohesivo, content items can be moved and copied between branches of the content tree. These operations, like in your computer’s file system, can apply both to individual content items and folders or groups. ![Content organization operations](https://doc.ibexa.co/en/saas/content_management/img/content_arrangement.png) Content items can be hidden when necessary, for example, until a certain event, like a Holiday Sale, or Board announcement comes. Hidden content items aren't visible to website visitors and are greyed out in the content tree. ![Hidden content item](https://doc.ibexa.co/en/saas/content_management/img/hidden_content_item.png) Editors can also move obsolete content items to Trash, and ultimately delete them. ![Delete confirmation dialog box](https://doc.ibexa.co/en/saas/content_management/img/delete_confirmation.png) ## Content classification There are multiple tools within Cohesivo that help content managers classify content or restrict access to content to certain recipients. ### Taxonomy With taxonomy you can create tags or keywords within a tree structure and assign them to content items. This way you can classify content and make it easier for end users to find the content they need, or browse and view content from a category that suits them best. ![Taxonomy principles](https://doc.ibexa.co/en/saas/content_management/img/taxonomy.png) ### Access control When your Cohesivo instance has multiple contributors and visitors, administrators can give them access to different areas of the website and different capabilities. It's done by creating roles, with each role having a different set of [permissions](https://doc.ibexa.co/en/saas/permissions/permission_overview/index.md), the most fitting example being the `content/edit` permission limited to an `Articles/BookReviews/Historical` subtree of the content tree. In the next steps, after you create user groups, you’d assign roles to these groups, and add individual users to each of such groups. For more technical information about permissions and limitations, see [Permission use cases](https://doc.ibexa.co/en/saas/permissions/permission_use_cases/index.md). There are, however, mechanisms to control access to content with even more convenience. ### Sections You can divide your content tree into nominal parts to better organize it. Once you have defined sections, for example, Media or Forms, and assigned them to content items, you can decide which roles have access to which section of the tree. The setting is inherited, which means that a child content item inherits a value of this setting from its parent. Changing a section setting doesn't result in moving a content item to a different location within a content tree. ![Members of the Media Section](https://doc.ibexa.co/en/saas/content_management/img/sections.png) ### Object states While reviewing the details of each individual content item in your content tree, you can assign a state to it, for example, “Locked” or “Not locked”. Then you can set a permission that allows or denies users access to content items in a specific state. This setting isn't inherited. ![Object states in content item’s Details](https://doc.ibexa.co/en/saas/content_management/img/object_states.png) ### User segments Although segments aren't meant to classify content, they could fall into this category, because their role is about targeting users, and not controlling their access to content. With segments, you can reach specific groups, or categories, of visitors with specific information about content or products that could be of their interest. For example, you can build Pages that contain different recommendations, depending on who is visiting them. ![A segment group with two user segments](https://doc.ibexa.co/en/saas/content_management/img/user_segments.png) ## How to get started Once you have integrated the headless implementation, installed a local instance of Cohesivo or set up an instance on Ibexa Cloud, you're ready to employ the content management features to good use. Since content management is an ongoing process, and, in your implementation, you might prefer focusing on other areas of configuration, the order of operations below is by all means conventional. **1. Create a content model** Any content that you might want to deliver to a viewer can be structured and split into smaller elements. Reverse-engineer the intended concepts into individual fields, which can be categorized, and then picked from categories and combined into content items. Reuse existing fields types or [customize them to fit your needs](https://doc.ibexa.co/en/saas/content_management/field_types/create_custom_generic_field_type/index.md), then [create content types](https://doc.ibexa.co/projects/userguide/en/6.0/content_management/create_edit_content_items/). **2. Define permissions** Although this step isn't directly related to content management, it's a good time to [set up user roles and permissions](https://doc.ibexa.co/projects/userguide/en/6.0/permission_management/work_with_permissions/), which users would need to work with content. **3. Author content** [Create various content items](https://doc.ibexa.co/projects/userguide/en/6.0/content_management/create_edit_content_items/), such as pages, articles, forms, or media. While you fill fields with content, several actions are there to help you with your task. You can pause and resume the work, preview the results, or send content for review. ![Send to review](https://doc.ibexa.co/en/saas/content_management/img/send_to_review.png) **4. Publish** Again, this isn't part of content management, but at this point you can [publish](https://doc.ibexa.co/projects/userguide/en/6.0/content_management/publish_instantly/) it right away or [schedule content for publication](https://doc.ibexa.co/projects/userguide/en/6.0/content_management/schedule_publishing/). **5. Organize content** Organize the content of your website by copying or moving content items, [controlling Locations and URL addresses](https://doc.ibexa.co/projects/userguide/en/6.0/content_management/content_organization/manage_locations_urls/). Then work with Tags, sections and object states to [classify](https://doc.ibexa.co/projects/userguide/en/6.0/content_management/content_organization/classify_content/#sections) it. ## Benefits The most important benefits of using Content management capabilities of Cohesivo can be gathered into the following groups: 1. Content management capabilities help reduce the effort required to maintain, administer, and distribute digital content, so that you can focus on business operations. 2. Segmentation, translations, and taxonomy make it possible to assist and target visitors from different backgrounds and markets. 3. Granular access control ensures that no content in your control lands before the unauthorized eyes. ## Use cases Cohesivo’s capabilities prove indispensable in many applications. ### Corporate website The most common use case for a comprehensive content management system like Cohesivo would be creating and maintaining a multinational company’s digital presence, with both public and intranet channels, multiple websites with overlapping content structures, and business partners and end-customers alike wanting to connect through different channels to access public and classified content. ### B2C web store Content management could lie at a foundation of a successful global web store, where customers connect through localized websites and branded mobile apps: individual products can have multiple variants with differing related assets, product descriptions must be available in multiple language versions, and access to certain areas of the store depends on both a country and a segment that the customer comes from. ### B2B store Extensive content management capabilities would prove themselves in a setting, where multiple buyers from different partner companies connect to an industry leader’s trading website, and they expect to find well organized product code (SKU) catalogs that contain basic product information. From there they would like to access detailed specifications, white papers and application notes. The same products could come with different brands and at different price points, depending on the customer segment or origin. # Content model > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). Cohesivo's content model relies on content items that are instances of content types and contain content fields. ## Content model overview The content structure in Cohesivo is based on content items. A content item represents a single piece of content, for example, an article, a blog post, an image, or a product. Each content item is an instance of a content type. > **Tip: Tip** > > An introduction to the content model for non-developer users is available in [User Documentation](https://doc.ibexa.co/projects/userguide/en/6.0/content_management/content_model/). ## Content items A content item consists of: - [Content information](#content-information) - [Fields](#fields), defined by the [content type](https://doc.ibexa.co/en/saas/administration/content_organization/content_types/index.md). The fields can cover data ranging from single variables and text lines to media files or blocks of formatted text. ### Content information General information about a content item is stored in a [`ContentInfo`](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-Core-Repository-Values-Content-ContentInfo.html) object. `ContentInfo` doesn't include fields. It contains following information: **`id`** - the unique ID of the Content object. These numbers aren't recycled, so if an item is deleted, its ID isn't reused when a new one is created. **`contentTypeId`** - the unique numerical ID of the content type, on which the content item is based. **`name`** - the name is generated automatically based on a [pattern specified in the content type definition](https://doc.ibexa.co/en/saas/administration/content_organization/content_types/#content-name-pattern). The name is in the main language of the content item. > **Note: Note** > > `name` is always searchable, even if the field(s) used to generate it aren't. **`sectionId`** - the unique number of the section to which the content item belongs. New content items are placed in the Standard section by default. This behavior can be changed, but content must always belong to some section. For more information, see [Sections](https://doc.ibexa.co/en/saas/administration/content_organization/sections/index.md). **`currentVersionNo`** - current version number is the number of the published version or of a newly created draft (which is 1). **`published`** - true if a published version exists, otherwise false. **`ownerId`** - ID of the user who initially created the content item. It's set by the system the first time the content item is published. The ownership of an item cannot be modified and doesn't change even if the owner is removed from the system. **`modificationDate`** - date and time when the content item was last modified. It's set by the system and cannot be modified manually, but changes every time the item is published again. **`publishedDate`** - date and time when the content item was published for the first time. It's set by the system and cannot be modified. **`alwaysAvailable`** - indicates if the content item is shown in the main language when it's not present in another requested language. It's [set per content type](https://doc.ibexa.co/en/saas/content_management/content_availability/index.md). **`remoteId`** - a global unique ID of the content item. Accepts up to 100 characters. Cannot contain non-printable characters and control sequences (anything in ASCII range `\x00` - `\x1F`). It's recommended to either let this value be generated by the Public PHP API as an MD5 hash, or at least to generate it as a hash (for example, one from SHA family). **`mainLanguageCode`** - the main language code of the content item. If the `alwaysAvailable` flag is set to true, the content item is shown in this language when the requested language doesn't exist. **`mainLocationId`** - identifier of the content item's main [location](https://doc.ibexa.co/en/saas/content_management/locations/index.md). **`status`** - status of the content item. It can have three statuses: 0 – *draft*, 1 – *published* and 2 – *archived*. When an item is created, its status is set to *draft*. After publishing the status changes to *published*. When a published content item is moved to Trash, the item becomes *archived*. If a published item is removed from the Trash (or removed without being put in the Trash first), it's permanently deleted. ![Diagram of an example content item](https://doc.ibexa.co/en/saas/content_management/img/content_model_item_diagram.png) The fields of a content item are defined by the content type to which the content item belongs. ## Fields A field is the smallest unit of storage in the content model and the building block of all content items. Every field belongs to a field type. Beyond the built-in set of field types, you can [create your own](https://doc.ibexa.co/en/saas/content_management/field_types/create_custom_generic_field_type/index.md). ### Field value validation The values entered in a field may undergo validation, which means the system makes sure that they're correct for the chosen field type and can be used without a problem. Validation depends on the settings of a particular field type. It cannot be turned off for a field if its field type supports it. ### Field details Aside from the field type, the field definition in a content type provides the following information: **Name** – a user-friendly name that describes the field. This name is used in the interface, but not internally by the system. It can consist of letters, digits, spaces, and special characters (the maximum length is 255 characters). If no name is provided, a unique one is automatically generated. **Identifier** – an identifier for internal use, for example, in configuration files, templates, or PHP code. It can only contain lowercase letters, digits and underscores (the maximum length is 50 characters). This identifier is also used in name patterns for the content type. **Description** – a detailed description of the field. **Required** – a flag which indicates if the field is required for the system to accept the content item. By default, if a field is flagged as Required, a user isn't able to publish a content item without filling in this field. > **Note: Note** > > You can use the `ContentService::validate()` method to decide whether the required fields or whole content items are checked for completeness at other stages of the editing process. > > The Required flag is in no way related to field validation. A field's value is validated whether the field is set as required or not. **[Searchable](https://doc.ibexa.co/en/saas/search/search/index.md)** – a flag which indicates if the value of the field is indexed for searching. The Searchable flag isn't available for some fields, because some field types don't allow searching through their values. **[Translatable](https://doc.ibexa.co/en/saas/multisite/languages/languages/index.md)** – a flag which indicates if the value of the field can be translated. It's independent of the field type, which means that even fields such as "Float" or "Image" can be set as translatable. Depending on the field type, there may also be other, specific information to fill in. For example, the "Country" field type allows you to select the default country, and to allow selecting multiple countries at the same time. ![Diagram of content model](https://doc.ibexa.co/en/saas/content_management/img/content_model_diagram.png) > **Tip: Tip** > > You can disable the possibility to edit specific field details per field type by [adding custom service definition for `ModifyFieldDefinitionsCollectionTypeExtension`](https://doc.ibexa.co/en/saas/content_management/field_types/customize_field_type_metadata/index.md). ## Content versions Each content item can have multiple versions. Each version has one of the following statuses: *draft*, *archived* or *published*. A new version is created every time a content item is edited. The previous published version isn't modified. Only one version can be published at the same time. When you publish a new version, the previous published version changes its status to Archived. The number of preserved archived versions is set in `ibexa.repositories.default.options.default_version_archive_limit`. By default it's set to 5. A new version is also created when a new [language](https://doc.ibexa.co/en/saas/multisite/languages/languages/index.md) is added to the content item. ## Products Products are a special type of content that holds products you can manage with the product catalog capabilities. For more information, see [Product catalog](https://doc.ibexa.co/en/saas/product_catalog/product_catalog/index.md). # Locations > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). Locations hold published content items and can be used to control visibility. When a new content item is published, it's automatically placed in a new location. All locations form a tree which is the basic way of organizing content in the system. Every published content item has a location and, as a consequence, also a place in this tree. ![Content tree - locations](https://doc.ibexa.co/en/saas/content_management/img/content_management_tree_locations.png "Content tree - locations") A content item receives a location only once it has been published. This means that a new unpublished draft doesn't have a location yet. You can find drafts in the **Drafts** tab in the **Content** menu. ![Drafts](https://doc.ibexa.co/en/saas/content_management/img/content_management_drafts.png "Drafts") A content item can have more than one location. It's then present in two or more places in the tree. For example, an article can be at the same time under "Local news" and "Sports news". Even in such a case, one of these places is always the main location. You can change the main location in the back office in the **Locations** tab, or [through the API](https://doc.ibexa.co/en/saas/content_management/content_api/managing_content/#changing-the-main-location). ![Locations](https://doc.ibexa.co/en/saas/content_management/img/content_management_locations.png "Locations") ## Top level locations The content tree is hierarchical. It has an empty root location at the top and a structure of dependent locations below it. Every location (aside from the root) has one parent location and can have any number of children. Top level locations are direct children of the root of the tree. The root has location ID 1, isn't related to any content items and should not be used directly. Under this root there are preset top level locations in each installation which cannot be deleted. ### Content The top level location for the actual contents of a site can be viewed by selecting the **Content structure** tab in the Content mode interface. ![Content structure](https://doc.ibexa.co/en/saas/content_management/img/content_management_tree.png "Content structure") This part of the tree is typically used, for example, for organizing folders, articles, or information pages. The default ID number of this location is 2, but it can be [modified via configuration](https://doc.ibexa.co/en/saas/administration/configuration/repository_configuration/#top-level-locations). It contains a Folder content item. ### Media **Media** is the top level location which stores and organizes information that is frequently used by content items located below the **Content** node. ![Media](https://doc.ibexa.co/en/saas/content_management/img/content_management_media.png "Media") It usually contains images, animations, documents and other files. The default ID number of the **Media** location is 43, but it can be [modified via configuration](https://doc.ibexa.co/en/saas/administration/configuration/repository_configuration/#top-level-locations). It contains a Folder content item. ### Users **Users** is the top level location that contains the built-in system for managing user accounts. ![Users in Admin panel](https://doc.ibexa.co/en/saas/administration/img/admin_panel_users.png "Users in Admin panel") A user is simply a content item of the user account content type. The users are organized within user group content items below this location. In other words, the **Users** location contains the actual users and user groups, which can be viewed by selecting the **Users** tab in the **Admin** Panel. The default ID number of the **Users** location is 5. It contains user group content items. ### Forms (Experience) **Forms** is the top level location that is intended for Forms created using the [Form Builder](https://doc.ibexa.co/projects/userguide/en/6.0/content_management/work_with_forms/#create-forms). ![Forms](https://doc.ibexa.co/en/saas/content_management/img/content_management_forms.png "Forms") ### Other top level locations You should not add any more content directly below location 1, but instead store any content under one of those top-level locations. ## Location visibility Location visibility allows you to control which parts of the content tree are available on the front page. ![Location visibility](https://doc.ibexa.co/en/saas/content_management/img/content_management_visibility.png "Location visibility") Once a content item is published, it cannot be un-published. When the location of a content item is hidden, the system doesn't display it on the website. > **Caution: Visibility and permissions** > > The [visibility switcher](https://doc.ibexa.co/en/saas/content_management/locations/#location-visibility) is a convenient feature for withdrawing content from the frontend. It acts as a filter in the frontend by default. You can choose to respect it or ignore it in your code. It isn't permission-based, and **doesn't restrict access to content**. Hidden content can be read through other means, like the REST API. > > If you need to restrict access to a given content item, you could create a role that grants read access for a given [**Section**](https://doc.ibexa.co/en/saas/administration/content_organization/sections/index.md) or [**Object State**](https://doc.ibexa.co/en/saas/administration/content_organization/object_states/index.md), and set a different section or object state for the given content. Or use other permission-based [**Limitations**](https://doc.ibexa.co/en/saas/permissions/limitations/index.md). If a content item is hidden, it's invisible in all its locations. If a location is hidden, all of its descendants in the tree are hidden as well. This means that there are three different visibility statuses: - Visible - Hidden - Hidden by superior All locations and content items are visible by default. If a location is made invisible manually, its status is set to Hidden. All locations under it change status to Hidden by superior. A content item is Hidden by superior only in locations in which it has a parent location with the Hidden status. In the following example, the **Content item 1** is Hidden by superior in the **Location A** while still visible in the **Location B**. ![Visibility in two locations](https://doc.ibexa.co/en/saas/content_management/img/locations_visibility.png) From the visitor's perspective a location behaves the same whether its status is Hidden or Hidden by superior – it's unavailable on the front page. The difference is that a location Hidden by superior cannot be revealed separately from their parent(s). It only becomes visible once all of its parent locations are made visible again. A Hidden by superior status doesn't override a Hidden status. This means that if a location is Hidden manually and later one of its ancestors is hidden as well, the first location's status doesn't change – it remains Hidden (not Hidden by superior). If the ancestor location is made visible again, the first location still remains hidden. The way visibility works can be illustrated using the following scenarios: ### Hiding a visible location ![Hiding a visible location](https://doc.ibexa.co/en/saas/content_management/img/node_visibility_hide.png) When you hide a location that was visible before, it gets the status Hidden. Its child locations are Hidden by superior. The visibility status of child locations that were already Hidden or Hidden by superior doesn't change. ### Hiding a location which is Hidden by superior ![Hiding a location which is Hidden by superior](https://doc.ibexa.co/en/saas/content_management/img/node_visibility_hide_invisible.png) When you explicitly hide a location which was Hidden by superior, it gets the status Hidden. Since the underlying locations are already either Hidden or Hidden by superior, their visibility status doesn't changed. ### Revealing a location with a visible ancestor ![Revealing a location with a visible ancestor](https://doc.ibexa.co/en/saas/content_management/img/node_visibility_unhide1.png) When you reveal a location which has a visible ancestor, this location and its children become visible. However, child locations that were explicitly hidden by a user keep their Hidden status (and their children remain Hidden by superior). ### Revealing a location with a Hidden ancestor ![Revealing a location with a Hidden ancestor](https://doc.ibexa.co/en/saas/content_management/img/node_visibility_unhide2.png) When you reveal a location that has a Hidden ancestor, it **doesn't** become Visible itself. Because it still has invisible ancestors, its status changes to Hidden by superior. > **Tip: In short** > > A location can only be Visible when all of its ancestors are Visible as well. ### Visibility mechanics The visibility mechanics are controlled by two flags: Hidden flag and Invisible flag. The Hidden flag informs whether the node has been hidden by a user or not. A raised Invisible flag means that the node is invisible either because it was hidden by a user or by the system. Together, the flags represent the three visibility statuses: | Hidden flag | Invisible flag | Status | | ----------- | -------------- | ------------------------------------------------------------------------------------------------------------------------ | | - | - | The location is visible. | | 1 | 1 | The location is invisible and it was hidden by a user (Hidden). | | - | 1 | The location is invisible and it was hidden by the system because its ancestor is hidden/invisible (Hidden by superior). | > **Note: Note** > > Displaying visible or hidden locations in governed by the [`Visibility` Search Criterion](https://doc.ibexa.co/en/saas/search/criteria_reference/visibility_criterion/index.md) # Content Relations > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). Content Relations control links between different content items, either created explicitly or by linking inside RichText fields. Content items are located in a tree structure through the locations they're placed in. However, content items themselves can also be related to one another. ![Content Relations](https://doc.ibexa.co/en/saas/content_management/img/content_management_relations.png "Content Relations") A **Relation** can exist between any two content items in the repository. For example, images are linked to news articles they're used in. Instead of using a fixed set of image attributes, the images are stored as separate content items outside the article. In the system you can find different types of Relations. Content can have Relations on item or on field level. *Relations at field level* are created using one of two special field types: Content relation (single) and Content relations (multiple). These fields allow you to select one or more other content items in the field value, which are linked to these fields. *Relations at content item level* can be of three different types: - *Common Relations* are created between two content items using the public PHP API. - *RichText linked Relations* are created using a field of the RichText type. When an internal link (a link to another location or content item) is placed in a RichText field, the system automatically creates a Relation. The Relation is automatically removed from the system when the link is removed from the content item. - *RichText embedded Relations* also use a RichText field. When an Embed element is placed in a RichText field, the system automatically creates a Relation between the embedded content item and the one with the RichText field. The Relation is automatically removed from the system when the link is removed from the content item. # Content availability > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). Control the availability of content items with relation to translations by using the Default content availability flag. The Default content availability flag enables you to control whether content is available when its translation is missing. You can set the flag in content type definition by checking the "Make content available even with missing translations" option. It's automatically applied to any new content item of this Type. ![Default content availability](https://doc.ibexa.co/en/saas/content_management/img/availability_flag.png "Default content availability") A content item with this flag is available in its main language even if it's not translated into the language of the current SiteAccess. Without the flag, a content item isn't available at all if it doesn't have a language version corresponding to the current SiteAccess. > **Note: Note** > > There is currently no way in the back office to edit the Content availability flag for an already published content item. > > To do this via [PHP API](https://doc.ibexa.co/en/saas/content_management/content_api/creating_content/#updating-content), set the [`alwaysAvailable` property](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-Core-Repository-Values-Content-ContentMetadataUpdateStruct.html#property_alwaysAvailable) of the Content metadata. The Default availability flag is used for the out-of-the box content types representing content that should always be visible to the user, such as media files or user content items. You can also use it for organizational content types. For example, you can assign the flag to a Blog content type which is intended to contain Blog Posts in multiple languages. If the Blog is in English only, it would not be visible for readers using the Norwegian or German SiteAcceses. However, if you set the default availability flag for the Blog content type, it's displayed to them in English (if it's set as a main language) and enables the users to browse individual posts in other languages. # Taxonomy > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). A taxonomy uses tags to categorize and organize content Taxonomies (**Tags**) allow you to organize content to make it easy for your site users to browse and to deliver content appropriate for them. Taxonomies are classifications of logical relationships between content. In Cohesivo you can create many taxonomies, each with many tags. The platform mechanism enables creating any entities with a tree structure and assign them to a content item. Default tag configuration is available in `config/packages/ibexa_taxonomy.yaml` The associated content type is `tag`. ```yaml ibexa_taxonomy: taxonomies: tags: parent_location_remote_id: taxonomy_tags_folder content_type: tag field_mappings: identifier: identifier parent: parent name: name ``` ## Configuration keys - `ibexa_taxonomies` - section responsible for taxonomy structure where you can [configure other taxonomies](#customize-taxonomy-structure) - `ibexa_taxonomies.tags.parent_location_remote_id` - Remote ID for location where new content items representing tags are created - `ibexa_taxonomies.tags.content_type` - Content type identifier which stands for the tags - `ibexa_taxonomies.tags.field_mappings` - field types map of a content type which taxonomy receives information about the tag from. Three fields are available: `identifier`, `parent` and `name`. The identifiers correspond to field names defined in the content type. The `name` field is used to automatically generate an identifier. ## Customize taxonomy structure You can create other taxonomies than the one predefined in the system, for example a Content category. To do it, first, create a new container to store the new taxonomy's items, for example a folder named "Content categories". Next, under the `ibexa_taxonomy.taxonomies` [key](https://doc.ibexa.co/en/saas/administration/configuration/configuration/#configuration-files) add the following configuration: ```yaml ibexa_taxonomy: taxonomies: # existing keys content_categories: parent_location_remote_id: content_type: content_category field_mappings: identifier: category_identifier parent: parent_category name: name ``` Replace `` with the new container's location remote ID. Translate the configuration identifier in the `ibexa_taxonomy` domain by, for example, creating a `translations/ibexa_taxonomy.en.yaml` file containing the following: ```yaml taxonomy.content_categories: 'Content categories' ``` Then, create a content type with `content_category` identifier and include the following field definitions: - `name` of `ibexa_string` type and required. Use this field, as ``, for content name pattern. - `category_identifier` of `ibexa_string` type and required. - `parent_category` of `ibexa_taxonomy_entry` type and not required. In its Taxonomy drop-down menu, select Content categories (or `taxonomy.content_categories` if no translation has been provided). Finish taxonomy setup by creating a new Content category named Root with identifier `content_categories_root` under the previously created container folder named Content categories. To use this new taxonomy, add an `ibexa_taxonomy_entry_assignement` field to a content type and select Content categories (or `taxonomy.content_categories`) in its Taxonomy drop-down setting. ### Hide Content tab The **Content** tab in taxonomy objects, for example, tags and categories, lists all Content assigned to the current taxonomy. You can hide the **Content** tab in the **Categories** view. In configuration add `assigned_content_tab` with the flag `false` (for other taxonomies this flag is by default set to `true`): ```yaml ibexa_taxonomy: taxonomies: # existing keys content_categories: parent_location_remote_id: content_type: content_category field_mappings: identifier: category_identifier parent: parent_category name: name assigned_content_tab: false ``` ### Hide menu item By default, for each taxonomy, a menu item is added to the main menu. You can hide this menu item by setting a value of the `register_main_menu` configuration key: ```yaml ibexa_taxonomy: taxonomies: # existing keys content_categories: # existing keys register_main_menu: false ``` For more information about available functionalities of tags, see [User Documentation](https://doc.ibexa.co/projects/userguide/en/6.0/content_management/taxonomy/taxonomy/). ## Hide delete button on large subtree The **Delete** button can be hidden when a taxonomy entry has many children. By default, the button is hidden when there are 100 children or more. The `delete_subtree_size_limit` configuration is [SiteAccess-aware](https://doc.ibexa.co/en/saas/multisite/siteaccess/siteaccess_aware_configuration/index.md), and can be set per SiteAccess, per SiteAccess group, or globally per default. For example: ```yaml ibexa: system: default: # or a SiteAccess, or a SiteAccess group taxonomy: admin_ui: delete_subtree_size_limit: 20 ``` ## Remove orphaned content items In some rare case, especially in Cohesivo v4.2 and older, when deleting parent of huge subtrees, some taxonomy entries aren't properly deleted, leaving content items that point to a non-existing parent. The command `ibexa:taxonomy:remove-orphaned-content` deletes those orphaned content item. It works on a taxonomy passed as an argument, and has two options that act as a protective measure against deleting data by mistake: - `--dry-run` to list deletable content items, without performing the deletion. - `--force` to effectively delete the orphaned content items. The following example first lists the orphaned content items for taxonomy `tags`, and then deletes them: ```bash php bin/console ibexa:taxonomy:remove-orphaned-content tags --dry-run php bin/console ibexa:taxonomy:remove-orphaned-content tags --force ``` ## Taxonomy suggestions Once the feature is [enabled](#enable-taxonomy-suggestions), with taxonomy suggestions, editors can pick from suggestions generated by an AI service based on selected fields like the product's or content item's name and description instead of having to manually browse through taxonomy trees and selecting [product categories](https://doc.ibexa.co/projects/userguide/en/6.0/product_catalog/work_with_product_categories/#assign-product-categories-by-editing-product-details) or [tags](https://doc.ibexa.co/projects/userguide/en/6.0/content_management/create_edit_content_items/#add-taxonomy-entries). Taxonomy suggestions build on existing [AI Actions](https://doc.ibexa.co/en/saas/ai/ai_actions/ai_actions_guide/index.md) functionality. The [`TaxonomyEmbeddingFieldProviderInterface`](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-Taxonomy-Embedding-TaxonomyEmbeddingFieldProviderInterface.html) service uses an existing taxonomy tree as reference, generating an embedding for each path in the taxonomy tree and storing it in the search index. For performance reasons, embeddings for the taxonomy tree entries are generated only in two cases: - when the search engine is reindexed, for example, right after you enable the feature and run the `ibexa:reindex` command - when an individual taxonomy entry is created or modified, it's embedding is updated When the editor creates or edits a content item or a product, they can request that the application suggests tags or product categories to be associated with the item. When it happens, the `Ibexa\Taxonomy\ActionHandler\TextToTaxonomyActionHandler` requests that an embedding is generated based on selected fields such as, for example, name and description. > **Note: Field selection** > > You select the actual text fields, whose values are used as source for the embedding generation, when you create an [AI action](https://doc.ibexa.co/projects/userguide/en/6.0/ai_actions/work_with_ai_actions/#create-ai-actions-that-use-ibexa-connect) that uses the `text-to-taxonomy` handler. The search engine then compares the generated embedding with the taxonomy path embeddings stored in its index. By default, it selects the three best-matching taxonomy paths and presents them to the editor as suggestions. The user can accept the suggestions, reject them, or request a new set of suggestions directly from the user interface. ### Enable Taxonomy suggestions Taxonomy suggestions are built into the product and do not require additional installation. However, before you can enable it, make sure the following prerequisites have been fulfilled: - [Search engine](https://doc.ibexa.co/en/saas/search/search_engines/search_engines/index.md): Taxonomy suggestions require a search engine that supports vector search. The feature has been tested to work with Elasticsearch or Solr 9.8.1+. - [AI Actions](https://doc.ibexa.co/en/saas/ai/ai_actions/ai_actions/index.md): To be able to process embeddings, Taxonomy suggestions require that you have the AI Actions configured to support the default [OpenAI](https://doc.ibexa.co/en/saas/ai/ai_actions/configure_ai_actions/#configure-access-to-openai) or the optional [Google Gemini](https://doc.ibexa.co/en/saas/ai/ai_actions/configure_ai_actions/#install-google-gemini-connector) service. > **Note: Alternative embeddings provider** > > To use Google Gemini as an alternative embeddings provider, you must also modify the default [taxonomy suggestions settings](https://doc.ibexa.co/en/saas/content_management/taxonomy/taxonomy/#change-embeddings-provider-to-google-gemini). #### Enable taxonomy embedding indexing Enable embedding indexing for taxonomy branches by changing the default setting from `false` to `true`. Toggle this setting at any time to enable or disable indexing of taxonomy embeddings. ```yaml ibexa: system: default: taxonomy: search: index_embeddings: true default_embedding_model: 'text-embedding-ada-002' ``` If you are happy with the default settings, clear the cache and reindex the search engine. ```bash php bin/console cache:clear php bin/console ibexa:reindex ``` #### Configure AI action Once you enable the Taxonomy suggestions feature, you must [configure an AI action](https://doc.ibexa.co/projects/userguide/en/6.0/ai_actions/work_with_ai_actions/#create-ai-actions-that-control-taxonomy-suggestions) that handles the generation of embeddings for newly created or edited content items or products. That's where you decide which exact fields from which content type should be used as input for embedding generation, how many suggestions are being presenter to the editor, and so on. After you do it, your users are be able to assign tags and/or product categories by using suggestions provided by an AI engine. ### Customize Taxonomy suggestions You can modify the default behavior of the Taxonomy suggestions model by changing various settings. #### Change default number of suggestions By default, the system returns three suggestions. You can change the default number if needed by altering the following setting: ```yaml ibexa_taxonomy: text_to_taxonomy: default_suggested_taxonomies_limit: 5 ``` You can also override this setting per AI action by editing its configuration. #### Change default fields parsed when generating suggestions The following setting decides which fields are used to generate suggestions by default. You can change the default setting, if needed. ```yaml ibexa: system: default: content_type_field_type_groups: configurations: vectorizable_fields: - ibexa_string - ibexa_text - ibexa_richtext ``` This way you can limit field selection to meaningful text fields and avoid unsupported field types. Like in the case of the number of suggestions, you can override this setting per AI action by editing its configuration. > **Tip: Tip** > > When selecting the input data for embedding creation, it's recommended to include only the essential information and limit the number of tokens sent. Otherwise, the embedding models can generate values that don't correspond closely to the actual meaning of the input. ### Change embedding generation models or embedding provider By default, the system comes with a set of OpenAI models that can be used for embedding generation. The following example shows these models listed in system configuration, together with a setting that controls what model is used when the editor requests taxonomy suggestions for an item. Also, here is where you can change the name of the model used by the provider, the embedding's dimensions, and other settings. ```yaml ibexa: system: default: embedding_models: text-embedding-3-small: name: 'text-embedding-3-small' dimensions: 1536 field_suffix: '3small' embedding_provider: 'ibexa_openai' text-embedding-3-large: name: 'text-embedding-3-large' dimensions: 3072 field_suffix: '3large' embedding_provider: 'ibexa_openai' text-embedding-ada-002: name: 'text-embedding-ada-002' dimensions: 1536 field_suffix: 'ada002' embedding_provider: 'ibexa_openai' default_embedding_model: 'text-embedding-ada-002' ``` > **Caution: Change both embedding generation models** > > When you change the default suggestions generation model, ensure that you update the `ibexa.system.default.taxonomy.search.default_embedding_model` setting that is used for taxonomy indexing purposes. Otherwise the taxonomy suggestions feature fails to find matching entries. #### Change embeddings provider to Google Gemini (LTS Update) Once you have installed and configured the [Google Gemini connector](https://doc.ibexa.co/en/saas/ai/ai_actions/configure_ai_actions/#install-google-gemini-connector), you can modify the default configuration to use the `ibexa_gemini` embedding provider and one of the [supported models](https://ai.google.dev/gemini-api/docs/embeddings): ```yaml ibexa: system: default: embedding_models: gemini_embedding_001_1536: name: 'gemini-embedding-001' dimensions: 1536 field_suffix: 'gemini_embedding_001_1536_dv' embedding_provider: 'ibexa_gemini' gemini_embedding_001_3072: name: 'gemini-embedding-001' dimensions: 3072 field_suffix: 'gemini_embedding_001_3072_dv' embedding_provider: 'ibexa_gemini' default_embedding_model: 'gemini_embedding_001_1536' # ... taxonomy: search: index_embeddings: true default_embedding_model: 'gemini_embedding_001_1536' ``` After you make the change: - Update the [Solr schema](https://doc.ibexa.co/en/saas/content_management/field_types/field_type_search/#configuring-solr) or [Elasticsearch mappings](https://doc.ibexa.co/en/saas/search/search_engines/elasticsearch/configure_elasticsearch/#fine-tune-the-search-results) by adding dynamic field definitions. Ensure that they match the dimensions (for example, 1536 or 3072) and suffixes that you defined above - Clear the cache and reindex the search engine ### Extending Taxonomy suggestions You can extend the feature by replacing the default code by exploring one of the following ideas. #### Replace the embedding provider By default, the system uses the `ibexa_openai` connector. You can add your own embedding provider if needed. To do it: - Implement the [`EmbeddingProviderInterface`](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-Core-Search-Embedding-EmbeddingProviderInterface.html) - Register the service with the `ibexa.embedding_provider` tag #### Extend the AI action form You can extend the `TextToTaxonomyOptionsType` AI action form by inheriting from `Ibexa\Bundle\Taxonomy\Form\Type\AbstractActionConfigurationOptions`. # Taxonomy API > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). Using the PHP API you can browse taxonomy entries, get their information and manage them. To manage taxonomies, use [`TaxonomyServiceInterface`](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-Taxonomy-Service-TaxonomyServiceInterface.html). ## Getting taxonomy entries To get a single taxonomy entry, you can use `TaxonomyServiceInterface::loadEntryById()`, and provide it with the numerical entry ID. Or pass entry identifier (with optionally a taxonomy identifier), and use `TaxonomyServiceInterface::loadEntryByIdentifier()`: ```php $entry = $this->taxonomyService->loadEntryByIdentifier('desks'); $output->writeln($entry->name . ' with parent ' . $entry->parent->name); ``` > **Note: Note** > > A taxonomy entry identifier is unique per taxonomy. If you have [several taxonomies](https://doc.ibexa.co/en/saas/content_management/taxonomy/taxonomy/#customize-taxonomy-structure), you can increase code readability by always passing the taxonomy identifier even when it's the default one. The default taxonomy is `tags` if it exists, else the first configured taxonomy (see `\Ibexa\Taxonomy\Service\TaxonomyConfiguration::getDefaultTaxonomyName` for details). > > ```php > /** > * @var array $springs > * @var \Ibexa\Contracts\Taxonomy\Service\TaxonomyServiceInterface $taxonomyService > */ > $springs[] = $taxonomyService->loadEntryByIdentifier('spring', 'tags'); > $springs[] = $taxonomyService->loadEntryByIdentifier('spring', 'events'); > $springs[] = $taxonomyService->loadEntryByIdentifier('spring', 'devices'); > ``` You can also get a taxonomy entry from the ID of its underlying content item, by using `TaxonomyServiceInterface::loadEntryByContentId()`. To get the root (main) entry of a given taxonomy, use `TaxonomyServiceInterface::loadRootEntry()` and provide it with the taxonomy name. To get all entries in a taxonomy, use `TaxonomyServiceInterface::loadAllEntries()`, provide it with the taxonomy identifier, and optionally specify the limit of results and their offset. The default taxonomy identifier is given by `TaxonomyConfiguration::getDefaultTaxonomyName` and is `'tags'` on a fresh installation. The default limit is 30. ```php $allEntries = $this->taxonomyService->loadAllEntries(null, 50); ``` To see how many entries is there, use `TaxonomyServiceInterface::countAllEntries()` with optionally a taxonomy identifier. To get all children of a specific taxonomy entry, use `TaxonomyServiceInterface::loadEntryChildren()`, provide it with the entry object, and optionally specify the limit of results and their offset. The default limit is 30: ```php $entryChildren = $this->taxonomyService->loadEntryChildren($entry, 10); foreach ($entryChildren as $child) { $output->writeln($child->name); } ``` ## Managing taxonomy entries You can move a taxonomy entry to a different parent by using `TaxonomyServiceInterface::moveEntry()`. Provide the method with two objects: the entry that you want to move and the new parent entry: ```php $entryToMove = $this->taxonomyService->loadEntryByIdentifier('standing_desks'); $newParent = $this->taxonomyService->loadEntryByIdentifier('desks'); $this->taxonomyService->moveEntry($entryToMove, $newParent); ``` You can also move a taxonomy entry by passing its target sibling entry to `TaxonomyServiceInterface::moveEntry()`. The method takes as parameters the entry you want to move, the future sibling, and a `position` parameter, which is either `TaxonomyServiceInterface::MOVE_POSITION_NEXT` or `TaxonomyServiceInterface::MOVE_POSITION_PREV`: ```php $sibling = $this->taxonomyService->loadEntryByIdentifier('school_desks'); $this->taxonomyService->moveEntryRelativeToSibling($entryToMove, $sibling, TaxonomyServiceInterface::MOVE_POSITION_PREV); ``` > **Note: Note** > > Taxonomy entry management functions triggers events you can listen to. For more information, see [Taxonomy events](https://doc.ibexa.co/en/saas/api/event_reference/taxonomy_events/index.md). ## Search You can search for content based on its taxonomy entry assignments by using the standard [`SearchService`](https://doc.ibexa.co/en/saas/search/search_api/index.md) with taxonomy-specific Search Criteria: | Criterion | Description | | -------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------- | | [TaxonomyEntryId](https://doc.ibexa.co/en/saas/search/criteria_reference/taxonomy_entry_id/index.md) | Find content assigned to a specific taxonomy entry | | [TaxonomyNoEntries](https://doc.ibexa.co/en/saas/search/criteria_reference/taxonomy_no_entries/index.md) | Find content that has no entries assigned from a given taxonomy | | [TaxonomySubtree](https://doc.ibexa.co/en/saas/search/criteria_reference/taxonomy_subtree/index.md) | Find content assigned to a taxonomy entry or any of its descendants | You can also use the [TaxonomyEntryId Aggregation](https://doc.ibexa.co/en/saas/search/aggregation_reference/taxonomyentryid_aggregation/index.md) to count content items per taxonomy entry. # Images > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). Manage image assets by using DAM systems, configuring image variations, optimizing and using placeholders. Images are an integral part of any website. They can serve as decoration and convey information. In Cohesivo, you can reuse them, normalize their file names, generate different size variations, resize images programmatically, or even define placeholders for missing ones. ## Images from DAM systems If your installation is connected to a DAM system, you can use images directly from a DAM system in your content. Specific [DAM configuration](https://doc.ibexa.co/en/saas/content_management/images/add_image_asset_from_dam/#dam-configuration) depends on the system that the installation uses. ## Reuse images You can store images in the media library as independent content items of a generic Image [content type](https://doc.ibexa.co/en/saas/administration/content_organization/content_types/index.md) to reuse them across the system. You do this by uploading images to an [ImageAsset](https://doc.ibexa.co/en/saas/content_management/field_types/field_type_reference/imageassetfield/index.md) field type. For an ImageAsset field to be reused, you must publish it. Only then is notification triggered, which states that an image has been published under the location and can now be reused. After you establish a media library, you can create [Relations](https://doc.ibexa.co/en/saas/content_management/content_relations/index.md) between the image content item and the main content item that uses it. ## Normalizing image file names If you use image files with unprintable UTF-8 characters in file names, you may come across a problem with images not displaying. Run the following command to normalize image file names: ```bash php bin/console ibexa:images:normalize-paths ``` Next, clear the cache: ```bash php bin/console cache:clear ``` and run the following: ```bash php bin/console liip:imagine:cache:remove ``` ## Configuring image variations With [image variations](https://doc.ibexa.co/en/saas/templating/image_variations/index.md) (image aliases) you can define and use different versions of the same image. You generate variations based on [filters](https://doc.ibexa.co/en/saas/templating/image_variations/#available-variation-filters) that modify aspects such as size and proportions, quality or effects. Image variations are generated with [LiipImagineBundle](https://github.com/liip/LiipImagineBundle), by using the underlying [Imagine library](https://imagine.readthedocs.io/en/latest/). The LiipImagineBundle bundle supports GD (default), Imagick or Gmagick PHP extensions, and enables you to define flexible filters in PHP. Image files are stored by using the `IOService,` and are completely independent from the Image field type. They're generated only once and cleared on demand, for example, on content removal). LiipImagineBundle only works on image blobs, so no command line tool is needed. For more information, see the [bundle's documentation](https://symfony.com/bundles/LiipImagineBundle/current/configuration.html). > **Caution: Code injection in images** > > Images must be treated like any other user-submitted data - as potentially malicious. > > - EXIF metadata of an image may contain for example, HTML, JavaScript, or PHP code. Cohesivo itself doesn't parse EXIF metadata, but third-party bundles must be secured against this eventuality. Make sure that metadata is properly escaped before use. > - Images may contain specially crafted flaws that exploit vulnerabilities in common image libraries like GD or Imagick, leading to code execution. It's important to keep these libraries up to date with security updates. ### Image URL resolution You can use LiipImagine's `liip:imagine:cache:resolve` command to resolve the path to image variations that are generated from the original image, with one or more paths as arguments. Paths to repository images must be relative to the `var//storage/images` directory, for example: `7/4/2/0/247-1-eng-GB/test.jpg`. For more information, see [LiipImagineBundle documentation](https://symfony.com/bundles/LiipImagineBundle/current/basic-usage.html#resolve-with-the-console). ## Resizing images You can resize all original images of a chosen content type with the following command. ```bash php bin/console ibexa:images:resize-original -f ``` You must provide the command with: - identifier of the image content type - identifier of the field that you want to affect - name of the image variation to apply to the images For example: ```bash php bin/console ibexa:images:resize-original image photo -f small_image ``` You can also pass two additional parameters: - `iteration-count` is the number of images to be recreated in a single iteration, to reduce memory use. The default value is `25`. - `user` is the identifier of a User with proper permission who performs the operation (`read`, `versionread`, `edit` and `publish`). The default value is `admin`. > **Caution: Caution** > > The `resize-original` command publishes a new version of each content item it modifies. ## Generating placeholder images With a placeholder generator you can download or generate placeholder images for any missing image. It proves useful when you're working on an existing database and are unable to download uploaded images to your local development environment, due to, for example, a large size of files. If the original image cannot be resolved, the `PlaceholderAliasGenerator::getVariation` method generates a placeholder by delegating it to the implementation of the [PlaceholderProvider](https://github.com/ibexa/core/blob/6.0/src/bundle/Core/Imagine/PlaceholderProvider.php) interface, and saves it under the original path. In Cohesivo, there are two implementations of the `PlaceholderProvider` interface: - [GenericProvider](#genericprovider) - [RemoteProvider](#remoteprovider) ### GenericProvider The [`GenericProvider`](https://github.com/ibexa/core/blob/6.0/src/bundle/Core/Imagine/PlaceholderProvider.php) package generates placeholders with basic information about the original image (see [example 1](#configuration-examples)). ![Placeholder image GenericProvider](https://doc.ibexa.co/en/saas/content_management/img/placeholder_info.jpg "Example of a generic placeholder image") ![Placeholder GenericProvider](https://doc.ibexa.co/en/saas/content_management/img/placeholder_generic_provider.png "Generic placeholder images on a page") | Option | Default value | Description | Required? | | ---------- | --------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------- | --------- | | fontpath | n/a | Path to the font file (\*.ttf). | Yes | | text | "IMAGE PLACEHOLDER %width%x%height%\\n(%id%)" | Text which is displayed in the image placeholder. %width%, %height%, %id% in it's replaced with width, height and ID of the original image. | | | fontsize | 20 | Size of the font in the image placeholder. | | | foreground | #000000 | Foreground color of the placeholder. | | | secondary | #CCCCCC | Secondary color of the placeholder. | | | background | #EEEEEE | Background color of the placeholder. | | ### RemoteProvider With the [`RemoteProvider`](https://github.com/ibexa/core/blob/6.0/src/bundle/Core/Imagine/PlaceholderProvider/RemoteProvider.php) you can download placeholders from: - remote sources, for example, (see [example 2](#configuration-examples)) - live version of a site (see [example 3](#configuration-examples)) ![Placeholder RemoteProvider - placecats.com](https://doc.ibexa.co/en/saas/content_management/img/placeholder_remote_provider.jpg "Remote placeholder images on a page") | Option | Default value | Description | | ----------- | ------------- | ------------------------------------------------------------------------------------------------------ | | url_pattern | '' | URL pattern. %width%, %height%, %id% in it's replaced with width, height and ID of the original image. | | timeout | 5 | Period of time before timeout, measured in seconds. | ### Semantic configuration Placeholder generation can be configured for each [`binary_handler`](https://doc.ibexa.co/en/saas/content_management/file_management/file_management/#handling-binary-files) under the `ibexa.image_placeholder` [configuration key](https://doc.ibexa.co/en/saas/administration/configuration/configuration/#configuration-files): ```yaml ibexa: # ... image_placeholder: : provider: options: ``` If there is no configuration assigned to the `binary_handler`, the placeholder generation is disabled. #### Configuration examples ##### Example 1 - placeholders with basic information about original image ```yaml ibexa: image_placeholder: default: provider: generic options: fontpath: '%kernel.project_dir%/src/Resources/font/font.ttf' background: '#EEEEEE' foreground: '#FF0000' text: 'MISSING IMAGE %%width%%x%%height%%' ``` ##### Example 2 - placeholders from remote source ```yaml ibexa: image_placeholder: default: provider: remote options: url_pattern: 'https://placecats.com/%%width%%/%%height%%' ``` ##### Example 3 - placeholders from live version of a site ```yaml ibexa: image_placeholder: default: provider: remote options: url_pattern: 'http://example.com/var/site/storage/%%id%%' ``` ## Support for SVG images You cannot store SVG images in Cohesivo by using the Image or ImageAsset field type. However, you can work things around by relying on the File field type and implementing a custom extension that lets you display and download files in your templates. > **Caution: Caution** > > SVG images may contain JavaScript, so they may introduce XSS or other security vulnerabilities. Make sure end users aren't allowed to upload SVG images, and be restrictive about which editors are allowed to do so. First, enable adding SVG files to content by removing them from the blacklist of allowed MIME types. To do it, overwrite `ibexa.site_access.config.default.io.file_storage.file_type_blacklist` defined in `Core/Resources/config/default_settings.yml` so that `svg` is removed from the blacklist. You can do it per SiteAccess or SiteAccess group by using [SiteAccess-aware configuration](https://doc.ibexa.co/en/saas/multisite/siteaccess/siteaccess_aware_configuration/index.md). Then, add a download route to the `config/routes.yaml` file: ```yaml app.svg_download: path: /asset/download/{contentId}/{fieldIdentifier}/{filename} defaults: { _controller: App\Controller\SvgController::downloadSvgAction } ``` It points to a custom controller that handles the downloading of the SVG file. The controller's definition (that you place in the `config/services.yaml` file under `services` key) and implementation are as follows: ```yaml services: # ... App\Controller\SvgController: public: true arguments: - '@ibexa.api.service.content' - '@ibexa.field_type.ibexa_binaryfile.io_service' - '@Ibexa\Core\Helper\TranslationHelper' ``` ```php query->has('version')) { $version = (int)$request->query->get('version'); } $content = $this->contentService->loadContent($contentId, null, $version); $language = $request->query->has('inLanguage') ? $request->query->get('inLanguage') : null; $field = $this->translationHelper->getTranslatedField($content, $fieldIdentifier, $language); if (!$field instanceof Field) { throw new InvalidArgumentException( sprintf( "%s field not present in content %d '%s'", $fieldIdentifier, $content->contentInfo->id, $content->contentInfo->name ) ); } $binaryFile = $this->ioService->loadBinaryFile($field->value->id); $response = new Response($this->ioService->getFileContents($binaryFile)); $disposition = $response->headers->makeDisposition( ResponseHeaderBag::DISPOSITION_INLINE, $filename ); $response->headers->set('Content-Disposition', $disposition); $response->headers->set('Content-Type', self::CONTENT_TYPE_HEADER); return $response; } } ``` To be able to use a proper link in your templates, you also need a dedicated Twig extension: ```php router->generate('app.svg_download', [ 'contentId' => $contentId, 'fieldIdentifier' => $fieldIdentifier, 'filename' => $filename, ]); } } ``` Now you can load SVG files in your templates by using generated links and a newly created Twig helper: ```twig {% set svgField = ibexa_field(content, 'file') %} ``` ## Image optimization JPEG images are optimized using the ImageMagic library, which is available out of the box. If you use other formats, such a PNG, SVG, GIF, or WEBP, and you use the Image Editor, to prevent images increasing in size when you modify them in the editor, you need to install additional image handling libraries. | Image format | Library | | ------------ | ---------------------------- | | JPEG | JpegOptim | | PNG | Either OptiPNG or Pngquant 2 | | SVG | SVGO 1 | | GIF | Gifsicle | | WEBP | cwebp | Install these libraries using your package manager, for example: ```bash sudo apt-get install optipng ``` ### Customizing image optimizers When the Image Editor saves a modified image, the system dispatches the [`ConfigureImageOptimizersEvent`](https://doc.ibexa.co/en/saas/api/event_reference/other_events/#image-editor) event before running the optimizer chain. You can listen to this event to customize the list of image optimizers at runtime. The following example shows how to remove the Pngquant optimizer to prevent grayscale conversion of low-saturation PNG images: ```php 'onConfigureOptimizers', ]; } public function onConfigureOptimizers(ConfigureImageOptimizersEvent $event): void { $event->removeOptimizer(Pngquant::class); } } ``` ## Embedding images in Rich Text The [RichText](https://doc.ibexa.co/en/saas/content_management/field_types/field_type_reference/richtextfield/index.md) field allows you to embed other content items within the field. Content items that are identified as images are rendered in the Rich Text field by using a dedicated template. You can determine content types that are treated as images and rendered. You do this by overriding the `ibexa.content_view.image_embed_content_types_identifiers` parameter, for example: ```yaml parameters: ibexa.content_view.image_embed_content_types_identifiers: [image, photo, banner] ``` You can set the template that is used when rendering embedded images in the `ibexa.default_view_templates.content.embed_image` container parameter: ```yaml parameters: ibexa.default_view_templates.content.embed_image: '@ibexadesign/content/view/embed/image.html.twig' ``` # Configure Image Editor > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). Configure image editor to crop, flip, and modify images. When a content item contains fields of the [`ibexa_image`](https://doc.ibexa.co/en/saas/content_management/field_types/field_type_reference/imageassetfield/index.md) type, users can perform basic image editing functions with the Image Editor. For more information, see [User Documentation](https://doc.ibexa.co/projects/userguide/en/6.0/image_management/edit_images/). > **Note: Note** > > The Image Editor doesn't support images that come from a Digital Asset Management (DAM) system. > **Note: Note** > > If you intend to modify images in formats other than JPEG in image editor, consider [adding a library to optimize them](https://doc.ibexa.co/en/saas/content_management/images/images/#image-optimization). ## Configuration You can modify the default settings to change the appearance or behavior of the Image Editor. You can also expand the default set of parameters to create buttons that may be required by custom features that you add by extending the Image Editor, for example, to enable changes to the color palette of an image. To do this, under the `ibexa.system..image_editor` [configuration key](https://doc.ibexa.co/en/saas/administration/configuration/configuration/#configuration-files) add a settings tree similar to the following example. The settings tree can contain one or more action groups. You can control the order of actions within a group by setting the `priority` parameter. You can also toggle the visibility of actions within the user interface. Image Editor settings are [SiteAccess-aware](https://doc.ibexa.co/en/saas/administration/configuration/dynamic_configuration/index.md). The following example sets the aspect ratio values and label names for buttons used by the Crop feature. ```yaml ibexa: system: default: image_editor: action_groups: default: id: default label: Default actions: crop: id: crop priority: 1 visible: true buttons: 1-1: label: 1:1 ratio: x: 1 y: 1 3-4: label: 3:4 ratio: x: 3 y: 4 4-3: label: 4:3 ratio: x: 4 y: 3 16-9: label: 16:9 ratio: x: 16 y: 9 custom: label: Custom ``` ### Image file size optimization #### Image quality You can configure the quality of the images modified in the Image Editor with the following configuration. The setting accepts values between 0 and 1, which corresponds to the compression level, with 0 being the strongest compression. The default quality is 0.92: ```yaml ibexa: system: default: image_editor: image_quality: 0.8 ``` #### Gaussian blur strength You can configure the gaussian blur strength applied during image optimization with the following configuration. ```yaml ibexa: system: default: image_editor: gaussian_blur_strength: 0.05 ``` The setting accepts float values between 0 and 10.0, where higher values increase blur and reduce file size, while lower values maintain sharpness. The default value is 0.05. Processing large images with high blur values (above 5) can be time-consuming and may result in request timeouts. Keep this in mind when configuring blur strength for environments that handle high-resolution images, and adjust [PHP's `max_execution_time`](https://www.php.net/manual/en/info.configuration.php#ini.max-execution-time) if needed. ### Additional information Each image can be accompanied by additional information that isn't visible to the user. By default, additional information stores the coordinates of the [focal point](https://doc.ibexa.co/projects/userguide/en/6.0/image_management/edit_images/#focal-point), but you can use this extension point to pass various parameters of custom features that you add by extending the Image Editor. To modify the value of additional information programmatically, you can set a value of the `Image` field by using the PHP API, for example: ```php use Ibexa\Core\FieldType\Image\Value as FieldValue; $value = new FieldValue([ 'data' => [ 'width' => '100', 'height' => '200', 'alternativeText' => 'test', 'mime' => 'image/png', 'id' => 1, 'fileName' => 'image.png', 'additionalData' => [ 'focalPointX' => 50, 'focalPointY' => 100, 'author' => 'John Smith', ], ], ]); ``` # Extend Image Editor > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). Add new functionalities to the image editor. With the Image Editor, users can do basic image modifications. You can configure the Image Editor's [default appearance or behavior](https://doc.ibexa.co/en/saas/content_management/images/configure_image_editor/index.md). You can also extend it by adding custom features. The following example shows how to extend the Image Editor by adding a button that draws a dot at a random location on the image. ## Create the JavaScript component file In `assets/random_dot/`, create the `random-dot.js` file with the following code of the React component: ```js import React, { useContext } from 'react'; import PropTypes from 'prop-types'; const { ibexa } = window; const IDENTIFIER = 'dot'; const Dot = () => { return (
    ); }; Dot.propTypes = {}; Dot.defaultProps = {}; export default Dot; ibexa.addConfig( 'imageEditor.actions.dot', // The ID ("dot") must match the one from the configuration yaml file { label: 'Dot', component: Dot, icon: ibexa.helpers.icon.getIconPath('form-radio'), // Path to an icon that will be displayed in the UI identifier: IDENTIFIER, // The identifier must match the one from the configuration yaml file }, true, ); ``` The code doesn't perform any action yet, you add the action in the following steps. ## Add configuration Configure the new Image Editor action under the `ibexa.system..image_editor` [configuration key](https://doc.ibexa.co/en/saas/administration/configuration/configuration/#configuration-files): ```yaml ibexa: system: default: image_editor: action_groups: default: id: default label: Default actions: dot: id: dot priority: 50 ``` ## Add entry to the Webpack configuration Once you create and configure the React component, you must add an entry to [the Webpack configuration](https://doc.ibexa.co/en/saas/tutorials/beginner_tutorial/3_customize_the_front_page/#configuring-webpack). In the root directory of your project, modify the `webpack.config.js` file by adding the following code: ```js const ibexaConfigManager = require('./ibexa.webpack.config.manager.js'); //... ibexaConfigManager.add({ ibexaConfig, entryName: 'ibexa-admin-ui-layout-js', newItems: [ path.resolve(__dirname, './assets/random_dot/random-dot.js'), ], }); ``` At this point you should be able to see a new button in the Image Editor's UI. > **Tip: Tip** > > Before you restart Cohesivo, run `php bin/console cache:clear` and `yarn encore ` to regenerate the assets. ## Expand the React component The button that you created above doesn't initiate any action yet. You must modify the JavaScript component to add a function to the button. ### Contexts When you create a React-based extension of the Image Editor, you can use a number of contexts that have the following functions: - CanvasContext - stores a canvas that displays the image, on which you can modify the image - ImageHistoryContext - stores the image history used by the Undo/Redo feature - AdditionalDataContext - stores additional data that is attached to the image, for example, focal point coordinates - TechnicalCanvasContext - stores a canvas, which you can use to draw elements that help modify the image, for example, a crop area or a grid, without interrupting with the actual image The last context is not used in this example. ### Draw a dot Modify the `random-dot.js` file by creating a function that uses the canvas context to draw a random dot on the image: ```js const drawDot = () => { const ctx = canvas.current.getContext('2d'); const positionX = Math.random() * canvas.current.width; const positionY = Math.random() * canvas.current.height; ctx.save(); ctx.fillStyle = '#ae1164'; ctx.beginPath(); ctx.arc(positionX, positionY, 20, 0, Math.PI * 2, true); ctx.fill(); ctx.restore(); saveInHistory(); }; ``` ### Store changes in history Create another function that uses the history context to store changes, so that users can undo their edits: ```js const saveInHistory = () => { const newImage = new Image(); newImage.onload = () => { dispatchImageHistoryAction({ type: 'ADD_TO_HISTORY', image: newImage, additionalData }); }; newImage.src = canvas.current.toDataURL(); }; ``` Complete component code ```js import React, { useContext } from 'react'; import PropTypes from 'prop-types'; import { CanvasContext, ImageHistoryContext, AdditionalDataContext, } from '../../vendor/ibexa/image-editor/src/bundle/ui-dev/src/modules/image-editor/image.editor.modules'; const { ibexa } = window; const IDENTIFIER = 'dot'; const Dot = () => { const [canvas, setCanvas] = useContext(CanvasContext); const [imageHistory, dispatchImageHistoryAction] = useContext(ImageHistoryContext); const [additionalData, setAdditionalData] = useContext(AdditionalDataContext); const saveInHistory = () => { const newImage = new Image(); newImage.onload = () => { dispatchImageHistoryAction({ type: 'ADD_TO_HISTORY', image: newImage, additionalData }); }; newImage.src = canvas.current.toDataURL(); }; const drawDot = () => { const ctx = canvas.current.getContext('2d'); const positionX = Math.random() * canvas.current.width; const positionY = Math.random() * canvas.current.height; ctx.save(); ctx.fillStyle = '#ae1164'; ctx.beginPath(); ctx.arc(positionX, positionY, 20, 0, Math.PI * 2, true); ctx.fill(); ctx.restore(); saveInHistory(); }; return (
    ); }; Dot.propTypes = {}; Dot.defaultProps = {}; export default Dot; ibexa.addConfig( 'imageEditor.actions.dot', { label: 'Dot', component: Dot, icon: ibexa.helpers.icon.getIconPath('form-radio'), identifier: IDENTIFIER, }, true, ); ``` Clear the cache and rebuild assets with the following commands: ```bash php bin/console cache:clear yarn encore dev ``` At this point you should be able to draw a random dot by clicking a button in the Image Editor's UI. # Add Image Asset from Digital Asset Management > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). Configure a Digital Asset Management connector. With the Digital Asset Management (DAM) system connector you can use assets such as images directly from the DAM in your content. ## DAM configuration You can configure a connection with a Digital Asset Management (DAM) system under the `ibexa.system..content.dam` [configuration key](https://doc.ibexa.co/en/saas/administration/configuration/configuration/#configuration-files). ```yaml ibexa: system: default: content: dam: [ dam_name ] ``` The configuration for each connector depends on the requirements of the specific DAM system. You can use the provided example DAM connector for [Unsplash](https://unsplash.com/), or [extend DAM support by creating a connector of your choice](#extend-dam-support-by-adding-custom-connector). To add the Unsplash connector to your system, add the `ibexa/connector-unsplash` bundle to your installation. ## Add Image Asset in Page Builder (Experience) To add Image Assets directly in the Page Builder, you can do it by using the Embed block. The example below shows how to add images from [Unsplash](https://unsplash.com/). First, in `templates/themes/standard/embed/`, create a custom template `dam.html.twig`: ```html+twig {% set dam_image = ibexa_field_value(content, 'image') %} {% if dam_image.source is not null %} {% set transformation = ibexa_dam_image_transformation(dam_image.source, '770px') %} {% set asset = ibexa_dam_asset(dam_image.destinationContentId, dam_image.source, transformation) %} {% set image_uri = asset.assetUri.path %} {% endif %} ``` The `770px` parameter in the template above is used to render the DAM image. It's the `unsplash` specific image variation and must be defined separately. Next, in `config/packages/ibexa.yaml`, set the `dam.html.twig` template for the `embed` view type that is matched for the content type, which you created for DAM images. For more information about displaying content, see [Content rendering](https://doc.ibexa.co/en/saas/templating/render_content/render_content/index.md). ```yaml ibexa: system: site: content_view: embed: image_dam: template: '@ibexadesign/embed/dam.html.twig' match: Identifier\ContentType: ``` In your [configuration file](https://doc.ibexa.co/en/saas/administration/configuration/configuration/#configuration-files) add the following configuration: ```yaml dam_unsplash: application_id: utm_source: variations: 770px: fm: jpg q: 80 w: 770 fit: max ``` You can customize the parameters according to your needs. For more information about supported parameters, see the [Unsplash documentation](https://unsplash.com/documentation#dynamically-resizable-images). In the back office, go to **Admin** > **Content types**. In the **Content** group, create a content type for DAM images, which includes the ImageAsset field. Now, when you use the Embed block in the Page Builder, you should see a DAM Image. For more information about block customization (defined templates, variations), see [Create custom block](https://doc.ibexa.co/en/saas/tutorials/page_and_form_tutorial/4_create_a_custom_block/index.md). ## Extend DAM support by adding custom connector To extend the DAM support built into Cohesivo, you must create a custom handler and transformation factory. > **Note: Wikimedia Commons licensing** > > Before you use Wikimedia Commons assets in a production environment, ensure that you comply with their [license requirements](https://commons.wikimedia.org/wiki/Commons:Reusing_content_outside_Wikimedia#How_to_comply_with_a_file's_license_requirements). ### Create DAM handler This class handles searching through Wikimedia Commons for images and fetching image assets. In `src/Connector/Dam/Handler` folder, create the `WikimediaCommonsHandler.php` file that resembles the following example, which implements [`search()`](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-Connector-Dam-Handler-Handler.html#method_search) to query the server and [`fetchAsset()`](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-Connector-Dam-Handler-Handler.html#method_fetchAsset) to return asset objects: ```php getPhrase()) . '&sroffset=' . $offset . '&srlimit=' . $limit ; $opts = [ 'http' => [ 'method' => 'GET', 'header' => [ 'User-Agent: ' . self::USER_AGENT, ], ], ]; $jsonResponse = file_get_contents($searchUrl, false, stream_context_create($opts)); if ($jsonResponse === false) { return new AssetSearchResult(0, new AssetCollection([])); } $response = json_decode($jsonResponse, true); if (!isset($response['query']['search'])) { return new AssetSearchResult(0, new AssetCollection([])); } $assets = []; foreach ($response['query']['search'] as $result) { $identifier = str_replace('File:', '', $result['title']); $assets[] = $this->fetchAsset($identifier); } return new AssetSearchResult( (int) ($response['query']['searchinfo']['totalhits'] ?? 0), new AssetCollection($assets) ); } public function fetchAsset(string $id): Asset { $metadataUrl = 'https://commons.wikimedia.org/w/api.php?action=query&prop=imageinfo&iiprop=extmetadata&format=json' . '&titles=File%3a' . urlencode($id) ; $opts = [ 'http' => [ 'method' => 'GET', 'header' => [ 'User-Agent: ' . self::USER_AGENT, ], ], ]; $jsonResponse = file_get_contents($metadataUrl, false, stream_context_create($opts)); if ($jsonResponse === false) { throw new \RuntimeException('Couldn\'t retrieve asset metadata'); } $response = json_decode($jsonResponse, true); if (!isset($response['query']['pages'])) { throw new \RuntimeException('Couldn\'t parse asset metadata'); } $pageData = array_values($response['query']['pages'])[0] ?? null; if (!isset($pageData['imageinfo'][0]['extmetadata'])) { throw new \RuntimeException('Couldn\'t parse image asset metadata'); } $imageInfo = $pageData['imageinfo'][0]['extmetadata']; return new Asset( new AssetIdentifier($id), new AssetSource('commons'), new AssetUri('https://commons.wikimedia.org/w/index.php?title=Special:Redirect/file/' . urlencode($id)), new AssetMetadata([ 'page_url' => "https://commons.wikimedia.org/wiki/File:$id", 'author' => $imageInfo['Artist']['value'] ?? null, 'license' => $imageInfo['LicenseShortName']['value'] ?? null, 'license_url' => $imageInfo['LicenseUrl']['value'] ?? null, ]) ); } } ``` Then, in `config/services.yaml`, register the handler as a service: ```yaml App\Connector\Dam\Handler\WikimediaCommonsHandler: tags: - { name: 'ibexa.platform.connector.dam.handler', source: 'commons' } ``` The `source` parameter passed in the tag is an identifier of this new DAM connector and is used in other places to glue elements together. ### Create transformation factory The transformation factory maps Cohesivo's image variations to corresponding variations from Wikimedia Commons. In `src/Connector/Dam/Transformation` folder, create the `WikimediaCommonsTransformationFactory.php` file that resembles the following example, which implements the [`TransformationFactory` interface](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-Connector-Dam-Variation-TransformationFactory.html): ```php $transformationParameters */ public function build(?string $transformationName = null, array $transformationParameters = []): Transformation { if (null === $transformationName) { return new Transformation(null, array_map(strval(...), $transformationParameters)); } $transformations = $this->buildAll(); if (array_key_exists($transformationName, $transformations)) { return $transformations[$transformationName]; } throw new \InvalidArgumentException(sprintf('Unknown transformation "%s".', $transformationName)); } public function buildAll(): array { return [ 'reference' => new Transformation('reference', []), 'tiny' => new Transformation('tiny', ['width' => '30']), 'small' => new Transformation('small', ['width' => '100']), 'medium' => new Transformation('medium', ['width' => '200']), 'large' => new Transformation('large', ['width' => '300']), ]; } } ``` Then register the transformation factory as a service: ```yaml App\Connector\Dam\Transformation\WikimediaCommonsTransformationFactory: tags: - { name: 'ibexa.platform.connector.dam.transformation_factory', source: 'commons' } ``` ### Register variations generator The variation generator applies map parameters coming from the transformation factory to build a fetch request to the DAM. The solution uses the built-in `URLBasedVariationGenerator` class, which adds all the map elements as query parameters to the request. For example, for an asset with the ID `Ibexa_Logo.svg`, the handler generates the Asset with [`AssetUri's URL`](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-Connector-Dam-AssetUri.html#method_getPath) equal to: `https://commons.wikimedia.org/w/index.php?title=Special:Redirect/file/Ibexa_Logo.svg` When the user requests a specific variation of the image, for example, "large", the variation generator modifies the URL and returns it in the following form: `https://commons.wikimedia.org/w/index.php?title=Special:Redirect/file/Ibexa_Logo.svg&width=300` For this to happen, register the variations generator as a service available for the custom `commons` connector: ```yaml commons_asset_variation_generator: class: Ibexa\Connector\Dam\Variation\URLBasedVariationGenerator tags: - { name: 'ibexa.platform.connector.dam.variation_generator', source: 'commons' } ``` ### Configure tab for "Select from DAM" modal To enable selecting an image from the DAM system, a modal window pops up with tabs and panels that contain different search interfaces. In this example, the search only uses the main text input. The tab and its corresponding panel are a service created by combining existing components, like in the case of other [back office tabs](https://doc.ibexa.co/en/saas/administration/back_office/back_office_tabs/back_office_tabs/index.md). The `commons_search_tab` service uses the `GenericSearchTab` class as a base, and the `GenericSearchType` form for search input. It is linked to the `commons` DAM source and uses the identifier `commons`. The DAM search tab is registered in the `connector-dam-search` [tab group](https://doc.ibexa.co/en/saas/administration/back_office/back_office_tabs/back_office_tabs/#tab-groups) using the `ibexa.admin_ui.tab` tag. ```yaml commons_search_tab: class: Ibexa\Connector\Dam\View\Search\Tab\GenericSearchTab public: false arguments: $identifier: 'commons' $source: 'commons' $name: 'Wikimedia Commons' $searchFormType: 'Ibexa\Connector\Dam\Form\Search\GenericSearchType' $formFactory: '@form.factory' tags: - { name: 'ibexa.admin_ui.tab', group: 'connector-dam-search' } ``` ### Create Twig template The template defines how images that come from Wikimedia Commons are displayed. In `templates/themes/standard/`, add the `commons_asset_view.html.twig` file that resembles the following example: ```html+twig {% extends '@ibexadesign/ui/field_type/image_asset_view.html.twig' %} {% block asset_preview %} {{ parent() }}
    Image {% if asset.assetMetadata.author %} by {{ asset.assetMetadata.author|striptags }}{% endif %} {% if asset.assetMetadata.license and asset.assetMetadata.license_url %} under {{ asset.assetMetadata.license }} {% endif %}.
    {% endblock %} ``` Then, register the template and a fallback template in configuration files. Replace `` with an [appropriate value](https://doc.ibexa.co/en/saas/multisite/siteaccess/siteaccess_aware_configuration/index.md) that designates the SiteAccess or SiteAccess group, for example, `default` to use the template everywhere, including the back office: ```yaml parameters: ibexa.site_access.config..image_asset_view_defaults: full: commons: template: '@@ibexadesign/commons_asset_view.html.twig' match: SourceBasedViewMatcher: commons default: template: '@@ibexadesign/ui/field_type/image_asset_view.html.twig' match: [] ``` ### Provide back office translation When the image asset field is displayed in the back office, a table of metadata follows. This example uses new fields, so you need to provide translations for their labels, for example, in `translations/ibexa_fieldtypes_preview.en.yaml`: ```yaml ibexa_image_asset.dam_asset.page_url: Image page ibexa_image_asset.dam_asset.author: Image author ibexa_image_asset.dam_asset.license: License ibexa_image_asset.dam_asset.license_url: License page ``` ### Add Wikimedia Commons connection to DAM configuration You can now configure a connection with Wikimedia Commons under the `ibexa.system..content.dam` key using the source identifier `commons`: ```yaml ibexa: system: default: content: dam: [ commons ] ``` Once you clear the cache, you can search for images to see whether images from the newly configured DAM are displayed correctly, including their variations. # Fastly Image Optimizer (Fastly IO) > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). Configure Fastly Image Optimizer. The Fastly Image Optimizer (Fastly IO) is an external service that provides real-time image optimization for multiple input and output formats. It serves and caches image requests from your origin server, making your website faster and more efficient. To be able to configure this feature, you need [Fastly IO subscription](https://www.fastly.com/documentation/guides/full-site-delivery/image-optimization/about-fastly-image-optimizer/). ## Enable shielding To use Fastly Image Optimizer, you first need a [working setup of Cohesivo and Fastly](https://doc.ibexa.co/en/saas/infrastructure_and_maintenance/cache/http_cache/reverse_proxy/#using-varnish-or-fastly) with shielding enabled. To enable shielding, follow the steps in [Fastly Developer Documentation](https://www.fastly.com/documentation/guides/concepts/shielding/#enabling-and-disabling-shielding). Remember to choose a shield location from the **Shielding** menu, as described in [Fastly User Documentation](https://www.fastly.com/documentation/guides/getting-started/hosts/shielding/#enabling-shielding). ## VCL configuration To manipulate your Fastly VCL configuration directly from the command line, you need to: - [install Fastly CLI](https://www.fastly.com/documentation/reference/tools/cli/#installing), - define `FASTLY_SERVICE_ID` and `FASTLY_KEY` environmental variables, - set optimizer restrictions by using the `ibexa_image_optimizer.vcl` file: ```vcl # Restrict optimizer by file path and extension if (req.url.ext ~ "(?i)^(gif|png|jpe?g|webp)$") { if (req.url.path ~ "^/var/([a-zA-Z0-9_-]+)/storage/images") { set req.http.x-fastly-imageopto-api = "fastly"; } } ``` You can customize what image formats are included, for example: `gif|png|jpe?g|webp`, and which paths should be used as a source of images, for example: `^/var/([a-zA-Z0-9_-]+)/storage/images`. For more configuration options, see [Enabling image optimization](https://www.fastly.com/documentation/reference/io/#enabling-image-optimization). To apply your modifications or use the default configuration as-is, you can upload the `.vcl` file from the command line: ```bash fastly vcl snippet create --name="Ibexa Image Optimizer" --version=active --autoclone --type recv --content=vendor/ibexa/fastly/fastly/ibexa_image_optimizer.vcl fastly service-version activate --version=latest ``` For more information about Fastly configuration and CLI usage examples, see [Configure and customize Fastly](https://doc.ibexa.co/en/saas/infrastructure_and_maintenance/cache/http_cache/fastly/index.md). ## Define SiteAccess for Fastly IO Fastly IO configuration is SiteAccess aware. You can define what handler should be used for a specific SiteAccess under `variation_handler_identifier` [configuration key](https://doc.ibexa.co/en/saas/administration/configuration/configuration/#configuration-files). You need to set it up as `fastly`, so Fastly IO can generate all image links. By default, it's set as `alias`, and it points to a built-in image optimizer. You can also set up a custom handler if your setup requires it. ```yaml ibexa: system: my_siteaccess: variation_handler_identifier: 'fastly' ``` You can also use environmental variables to configure a specific handler for a SiteAccess. See the example below to configure it with the `.env` file: ```bash IBEXA_VARIATION_HANDLER_IDENTIFIER="fastly" ``` ## Image configuration When you define image variation keys for Fastly IO, keep in mind that they should reflect variations in your original setup. The built-in image optimizer serves as backup to Fastly IO in case of misconfiguration, so it needs to be able to serve the same image variations. Fastly IO image filters aren't compatible with Ibexa built-in filters, so you aren't able to reflect your original filters accurately with Fastly. The script below helps you find replacement filters within Fastly configuration for the basic filters. For more optimization options on Fastly side, see [Fastly IO reference](https://www.fastly.com/documentation/reference/io/). To generate your original image configuration run: ```bash php bin/console ibexa:fastly:migrate-configuration ``` Paste the following configuration to define the same variations for Fastly IO: ```yaml ibexa: system: default: fastly_variations: reference: reference: original configuration: width: 600 height: 600 fit: bounds small: reference: reference configuration: width: 100 height: 100 fit: bounds tiny: reference: reference configuration: width: 30 height: 30 fit: bounds medium: reference: reference configuration: width: 200 height: 200 fit: bounds large: reference: reference configuration: width: 300 height: 300 fit: bounds gallery: reference: original configuration: { } ezplatform_admin_ui_profile_picture_user_menu: reference: reference configuration: width: 30 height: 30 fit: bounds crop: '30,30,x0,y0' ``` You can select defined image variations during content item creation in the image options. Variations can include different sizing options and other filters that are applied to the image. ![Fastly image variations](https://doc.ibexa.co/en/saas/content_management/img/fastly_variations.png) # RichText > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). RichText is a type of field that you add in any content item in Cohesivo and edit in Online Editor. RichText is a type of field that you add in any content item in Cohesivo and edit in Online Editor. - [Online Editor product guide](https://ez-systems-developer-documentation--3381.com.readthedocs.build/en/3381/content_management/rich_text/online_editor_guide/): Learn how to use the Online Editor, a tool that allows you to edit RichText Fields in any content item in Cohesivo. - [Extend Online Editor](https://ez-systems-developer-documentation--3381.com.readthedocs.build/en/3381/content_management/rich_text/extend_online_editor/): Add custom tags, styles and data attributes to enrich the functionality of the Online Editor. Change Online Editor configuration. - [Create custom RichText block](https://ez-systems-developer-documentation--3381.com.readthedocs.build/en/3381/content_management/rich_text/create_custom_richtext_block/): Create a custom Page block containing rich text. # Online Editor product guide > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). Learn how to use the Online Editor, a tool that allows you to edit RichText Fields in any content item in Cohesivo. ## What is Online Editor Online Editor is the interface for editing RichText fields in any content item in Cohesivo. It offers standard editing capabilities and extensibility points to customize the editing experience and the available elements. Online Editor is based on [CKEditor 5](https://ckeditor.com/ckeditor-5/). ## Availability Online Editor is available in all supported Cohesivo versions and editions. ## How to get started Online Editor is the default editing interface for all RichText fields. To start using it, create any content item with a RichText field (for example, based on the built-in Article content type) and edit this field. ## Capabilities ### Rich Text editor Online Editor covers all fundamental formatting options for rich text, such as headings, lists, tables, inline text formatting, anchors, and links. It also allows embedding other content from the repository, but also from Facebook, Twitter, or YouTube. #### Links All links added to a RichText field by using the link element are listed and can be managed in the [Link manager](https://doc.ibexa.co/en/saas/content_management/url_management/url_management/index.md). #### Distraction free mode While editing Rich Text fields, you can switch to distraction free mode that expands the workspace to full screen. ![Distraction free mode](https://doc.ibexa.co/en/saas/content_management/img/distraction_free_mode.png) For more information, see [Distraction free mode](https://doc.ibexa.co/projects/userguide/en/6.0/content_management/create_edit_content_items/#distraction-free-mode). ### Custom tags Custom tags are customizable RichText elements for which you can specify attributes and render them with custom templates. Custom tags can be created by means of specifying two things only: - YAML configuration - relevant Twig templates The YAML configuration defines a custom tag’s attributes, the template used to render it, and where in the toolbar the tag is available. See [Extend Online Editor](https://doc.ibexa.co/en/saas/content_management/rich_text/extend_online_editor/#configure-custom-tags) for a full example. ### Custom styles Custom styles allow specifying custom predefined templates for specific RichText elements. Custom styles differ from custom tags in that they don't have attributes configured. A custom style requires YAML configuration that points to a template used to render an elements with this style. See [Extend Online Editor](https://doc.ibexa.co/en/saas/content_management/rich_text/extend_online_editor/#configure-custom-styles) for a full example. ### Custom data attributes and CSS classes For each RichText element type, you can configure custom data attributes or CSS classes that the user can select when working in Online Editor. Custom data attributes allow adding new attributes to existing Rich Text elements, such as headings or lists, which are added in the form of `data-ezattribute-=""`. For more information, see [Extend Online Editor](https://doc.ibexa.co/en/saas/content_management/rich_text/extend_online_editor/#custom-data-attributes). Custom CSS classes work in a similar way, giving editor a choice of classes to add to any type of element. For more information, see [Extend Online Editor](https://doc.ibexa.co/en/saas/content_management/rich_text/extend_online_editor/#custom-css-classes). ### Plugins Online Editor is based on CKEditor 5, and you can use CKEditor's capabilities to [create plugins](https://doc.ibexa.co/en/saas/content_management/rich_text/extend_online_editor/#add-ckeditor-plugins) for the editor. ## Benefits ### Familiar editing tools Online editor offers rich text editing tools familiar to most editors and contributors, which allows quick adoption to the editorial flow. ![Familiar editing tools](https://doc.ibexa.co/en/saas/content_management/rich_text/img/familiar_editing_tools.png) The editor's toolbars can be customized and reorganized to for the specific project's needs. ### Customizable text elements The range of available text elements can be extended by offering custom elements and custom formatting options. Custom formatting options can be offered either as custom CSS classes that editors can add to specific elements, or as custom styles which can have their own templates. More extensive customization is available via custom tags: - completely custom RichText elements that you can fully configure - custom CKEditor 5 plugins ## Use cases ### Customizable Call to action buttons Online Editor extensibility offers a simple way to create custom elements such as Call to action (CTA) buttons. Creating a CTA custom tag lets you use a template to construct a button element. Then, you can add a link attribute to provide target for the button, and a style attribute with different presets to style its look. ![Call to action buttons](https://doc.ibexa.co/en/saas/content_management/rich_text/img/call_to_action_buttons.png) Refer to [Extend Online Editor](https://doc.ibexa.co/en/saas/content_management/rich_text/extend_online_editor/#link-tag) for a similar use case. ### Product marketing campaigns With the Online Editor, editors can embed products from the product catalog directly into RichText fields. Products can be embedded as block-level or inline elements. You can use it to weave marketing content around your product data, showcasing your product capabilities and bringing it closer to your customers. See [Embed products in content](https://doc.ibexa.co/en/saas/product_catalog/products/#embed-products-in-content) for details. ### Embed external resources Custom tags allow embedding content from external resources inside RichText fields. The built-in elements offer embedding of Twitter or Facebook posts, but you can extend the capability by embedding other resources. These can be, for example, 3D product, or real estate viewers. # Extend Online Editor > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). Add custom tags, styles and data attributes to enrich the functionality of the Online Editor. Change Online Editor configuration. Cohesivo users edit the contents of RichText fields, for example, in the Content box of a Page, by using the Online Editor. You can extend the Online Editor by adding custom tags and styles, defining custom data attributes, re-arranging existing buttons, grouping buttons into custom toolbar, and creating [custom buttons](https://ckeditor.com/docs/ckeditor4/latest/guide/widget_sdk_tutorial_1.html#widget-toolbar-button) and [custom plugins](https://ckeditor.com/docs/ckeditor4/latest/guide/dev_plugins.html). Online Editor is based on the CKEditor5. Refer to [CKEditor5 documentation](https://ckeditor.com/docs/ckeditor5/latest/index.html) to learn how you can extend the Online Editor with even more elements. For more information about extending the back office, see [Extend back office](https://doc.ibexa.co/en/saas/administration/back_office/back_office/index.md). ## Configure custom tags With custom tags, you can enhance the Online Editor with features that go beyond the built-in ones. You configure custom tags under the `ibexa_richtext` key. Start preparing the tag by adding a configuration file: ```yaml ibexa_fieldtype_richtext: custom_tags: factbox: template: '@ibexadesign/field_type/ibexa_richtext/custom_tags/factbox.html.twig' icon: '/bundles/ibexaadminuiassets/vendors/ids-assets/dist/img/all-icons.svg#info-square' # Optional is_inline: false # Optional label: 'Factbox custom tag' # Optional description: 'Highlight a piece of information in a box' # Optional attributes: name: type: string required: true # Optional label: 'My name attribute' # Optional style: type: choice required: true # Optional default_value: light choices: [light, dark] label: 'My style attribute' # Optional ibexa: system: admin_group: fieldtypes: ibexa_richtext: custom_tags: [factbox] toolbar: custom_tags_group: buttons: factbox: priority: 5 ``` The example enables the custom tag under the `admin_group` [SiteAccess group](https://doc.ibexa.co/en/saas/multisite/siteaccess/siteaccess/index.md), which controls where editors can use it. The custom tag renders in all SiteAccesses, including the front end. Custom tags can have as many attributes as needed. Supported attribute types are: - `string` - `number` - `boolean` - `link` - `choice` `choice` requires that you provide a list of options in the `choices` key. Provide your own SVG icon, or choose one from the built-in icons included in `all-icons.svg`. You must create your own file for the Twig template. Place the `factbox.html.twig` template in the `templates/themes//field_type/ibexa_richtext/custom_tags` directory: ```html+twig {{ encore_entry_link_tags('factbox') }}

    {{ params.name }}

    {{ content|raw }}
    ``` If an attribute isn't required, check if it's defined by adding a check in the template, for example: ```html+twig {% if params.your_attribute is defined %} ... {% endif %} ``` In this example, the `style` attribute is a `choice` attribute with `light` and `dark` as possible values. The selected value is available as `params.style` in the template. Use it to build an `ibexa-factbox--light` or `ibexa-factbox--dark` modifier class on the wrapping `div` element for styling. You can then define the corresponding CSS for each choice, for example by using [Webpack Encore and assets](https://doc.ibexa.co/en/saas/templating/assets/index.md). Create a `assets/scss/factbox.scss` file for styling the custom tag: ```css .ibexa-factbox--light { background-color: #f5f5f5; color: #202020; } .ibexa-factbox--dark { background-color: #202020; color: #f5f5f5; } ``` Then, register the file in `webpack.config.js` as an asset entry called `factbox`: ```js Encore.addStyleEntry('factbox', [ path.resolve(__dirname, './assets/scss/factbox.scss'), ]); ``` After you add the configuration, template, and asset files, clear the cache and run `yarn encore `. ### Provide translations for custom tags You can provide the label and description displayed for the custom tag and its attributes in the back office in one of two ways. #### Option 1: Manually add translations Add labels for the new tag by providing the translations manually in `translations/custom_tags.en.yaml`: ```yaml ibexa_richtext.custom_tags.factbox.label: 'Factbox' ibexa_richtext.custom_tags.factbox.description: 'Highlight a piece of information in a box' ibexa_richtext.custom_tags.factbox.attributes.name.label: 'Name' ibexa_richtext.custom_tags.factbox.attributes.style.label: 'Style' ibexa_richtext.custom_tags.factbox.attributes.style.choice.dark.label: 'Dark' ibexa_richtext.custom_tags.factbox.attributes.style.choice.light.label: 'Light' ``` This approach is quick, but doesn't work when your custom tag is defined in a bundle. The configuration and the labels are defined in separate files, making it easier to miss updating them when the custom tag changes. #### Option 2: Extract translation source texts from configuration To provide the translations with the custom tag configuration, specify the `label` and `description` keys for the custom tag itself, and a `label` key for each attribute. ```yaml ibexa_fieldtype_richtext: custom_tags: factbox: template: '@ibexadesign/field_type/ibexa_richtext/custom_tags/factbox.html.twig' icon: '/bundles/ibexaadminuiassets/vendors/ids-assets/dist/img/all-icons.svg#info-square' # Optional is_inline: false # Optional label: 'Factbox custom tag' # Optional description: 'Highlight a piece of information in a box' # Optional attributes: name: type: string required: true # Optional label: 'My name attribute' # Optional style: type: choice required: true # Optional default_value: light choices: [light, dark] label: 'My style attribute' # Optional ibexa: system: admin_group: fieldtypes: ibexa_richtext: custom_tags: [factbox] toolbar: custom_tags_group: buttons: factbox: priority: 5 ``` To make use of them, create a new service with `Ibexa\FieldTypeRichText\Translation\Extractor\CustomTagExtractor` as the class. If it has `choice` attributes, add an additional service with `Ibexa\FieldTypeRichText\Translation\Extractor\ChoiceAttributeExtractor` as the class. In both cases, add your custom tag's identifier to the `allowlist` argument: ```yaml services: app.extractor.custom_tag: class: Ibexa\FieldTypeRichText\Translation\Extractor\CustomTagExtractor arguments: $customTags: '%ibexa.field_type.richtext.custom_tags%' $domain: '%ibexa.field_type.richtext.custom_tags.translation_domain%' $allowlist: ['factbox'] tags: - name: jms_translation.extractor alias: app.translation_extractor.factbox app.extractor.custom_tag_choice: class: Ibexa\FieldTypeRichText\Translation\Extractor\ChoiceAttributeExtractor arguments: $customTags: '%ibexa.field_type.richtext.custom_tags%' $domain: '%ibexa.field_type.richtext.custom_tags.translation_domain%' $allowlist: ['factbox'] tags: - name: jms_translation.extractor alias: app.translation_extractor.factbox_choice ``` Then, create your own translation extraction configuration, and specify the Symfony services created above as extractors: ```yaml jms_translation: configs: app_translation_config: dirs: ['%kernel.project_dir%/src'] output_dir: '%kernel.project_dir%/translations' output_format: yaml extractors: - 'app.translation_extractor.factbox' - 'app.translation_extractor.factbox_choice' ``` Run the translation extraction: ```bash php bin/console translation:extract -c app_translation_config ``` This updates `translations/custom_tags.en.yaml` with the source texts taken from the configuration. If you omit `label` or `description`, the extraction uses the identifier of the custom tag or attribute as the source text. To provide translations for values of a `choice` attribute, `ChoiceAttributeExtractor` capitalizes the first letter of the value. For example, `light` and `dark` options become `Light` and `Dark`. Run the extraction again whenever you change the labels, descriptions, or attributes of the custom tag. ### Use custom tag Now you can use the tag. In the back office, create or edit a content item that has a RichText field type. In the Online Editor, click **Add**, and from the list of available tags select the FactBox tag icon. ![FactBox Tag](https://doc.ibexa.co/en/saas/content_management/img/custom_tag_factbox.png "FactBox Tag in the Online Editor") ### Inline custom tags You can also place custom tags inline with the following configuration: ```yaml ibexa_fieldtype_richtext: custom_tags: acronym: template: '@ibexadesign/field_type/ibexa_richtext/custom_tags/acronym.html.twig' icon: '/bundles/ibexaadminuiassets/vendors/ids-assets/dist/img/all-icons.svg#edit' is_inline: true attributes: # ... ``` `is_inline` is an optional key. The default value is `false`, therefore, if it's not set, the custom tag is treated as a block tag. ### Use cases #### Link tag You can configure a custom tag with a `link` attribute that offers a basic UI with text input. It's useful when migrating from eZ Publish to Cohesivo. The configuration is: ```yaml ibexa: system: admin_group: fieldtypes: ibexa_richtext: custom_tags: [linktag] toolbar: custom_tags_group: buttons: linktag: priority: 6 ibexa_fieldtype_richtext: custom_tags: linktag: template: '@ibexadesign/field_type/ibexa_richtext/custom_tags/linktag.html.twig' icon: '/bundles/ibexaadminuiassets/vendors/ids-assets/dist/img/all-icons.svg#link' is_inline: true attributes: title: type: string required: false description: type: string required: false color: type: choice required: false choices: [Red, Blue, Green] url: type: link required: false ``` Provide your own SVG icon, or choose one from the built-in icons included in `all-icons.svg`. Use your own file for the Twig template. The tag has the `url` attribute with the `type` parameter set as `link` (lines 30-31). Then create the `templates/themes//field_type/ibexa_richtext/custom_tags/linktag.html.twig` template: ```html+twig

    Custom link

    {% for attr_name, attr_value in params %}
    {{ attr_name }}: {{ attr_value }}
    {% endfor %} ``` Add labels for the tag by providing translations in `translations/custom_tags.en.yaml`: ```yaml ibexa_richtext.custom_tags.linktag.label: 'Link Tag' ibexa_richtext.custom_tags.linktag.attributes.title.label: 'Title' ibexa_richtext.custom_tags.linktag.attributes.description.label: 'Description' ibexa_richtext.custom_tags.linktag.attributes.color.label: 'Color' ibexa_richtext.custom_tags.linktag.attributes.url.label: 'URL' ``` Now you can use the tag. In the back office, create or edit a content item that has a RichText field type. In the Online Editor's toolbar, click **Show more items**, and from the list of available tags select the Link tag icon. ![Link Tag](https://doc.ibexa.co/en/saas/content_management/img/custom_tag_link.png "Link Tag in the Online Editor") #### Acronym You can create an inline custom tag that displays a hovering tooltip with an explanation of an acronym. ```yaml ibexa: system: admin_group: fieldtypes: ibexa_richtext: custom_tags: [acronym] toolbar: custom_tags_group: buttons: acronym: priority: 7 ibexa_fieldtype_richtext: custom_tags: acronym: template: '@ibexadesign/field_type/ibexa_richtext/custom_tags/acronym.html.twig' icon: '/bundles/ibexaadminuiassets/vendors/ids-assets/dist/img/all-icons.svg#edit' is_inline: true attributes: explanation: type: string ``` The `explanation` attribute contains the meaning of the acronym that is provided while editing in the Online Editor. Add labels for the tag by providing translations in `translations/custom_tags.en.yaml`: ```yaml ibexa_richtext.custom_tags.acronym.label: 'Acronym' ibexa_richtext.custom_tags.acronym.attributes.explanation.label: 'Explanation' ``` ![Adding an explanation to an Acronym custom tag](https://doc.ibexa.co/en/saas/content_management/img/oe_custom_tag_add_acronym.png) In the template file `acronym.html.twig` provide the explanation as attribute value to the title of the `abbr` tag: ```html+twig {{ content }} ``` ![Acronym custom tag](https://doc.ibexa.co/en/saas/content_management/img/oe_custom_tag_acronym.png) ## Configure custom styles You can extend the Online Editor with custom text styles. The styles are available in the text toolbar when a section of text is selected. There are two kinds of custom styles: block and inline. Inline styles apply to the selected portion of text only, while block styles apply to the whole paragraph. Start creating a custom style by providing configuration: - a global list of custom styles, defined under the node `ibexa_richtext.custom_styles`, - a list of enabled custom styles for a given `admin` SiteAccess or `admin_group` SiteAccess group, located under the node `ibexa.system..fieldtypes.ibexa_richtext.custom_styles` A sample configuration could look as follows: ```yaml ibexa: system: admin_group: fieldtypes: ibexa_richtext: custom_styles: [highlighted_block, highlighted_word] ibexa_fieldtype_richtext: custom_styles: highlighted_word: template: '@ibexadesign/field_type/ibexa_richtext/custom_styles/highlighted_word.html.twig' inline: true highlighted_block: template: '@ibexadesign/field_type/ibexa_richtext/custom_styles/highlighted_block.html.twig' inline: false ``` > **Note: Note** > > Currently, if you define these lists for a front site SiteAccess, it has no effect. Add labels for the new styles by providing translations in `translations/custom_styles.en.yaml`: ```yaml ibexa_richtext.custom_styles.highlighted_block.label: Highlighted block ibexa_richtext.custom_styles.highlighted_word.label: Highlighted word ``` ### Rendering The `template` key points to the template that is used to render the custom style. It's recommended that you use the [design engine](https://doc.ibexa.co/en/saas/templating/design_engine/design_engine/index.md). The template files for the front end could look as follows: - `templates/themes/standard/field_type/ibexa_richtext/custom_styles/highlighted_word.html.twig`: ```html+twig {% apply spaceless %}{{ content|raw }}{% endapply %} ``` - `templates/themes/standard/field_type/ibexa_richtext/custom_styles/highlighted_block.html.twig`: ```html+twig
    {% apply spaceless %}{{ content|raw }}{% endapply %}
    ``` Templates for Content View in the back office would be `templates/themes/admin/field_type/ibexa_richtext/custom_styles/highlighted_word.html.twig` and `templates/themes/admin/field_type/ibexa_richtext/custom_styles/highlighted_block.html.twig` (assuming that the back office SiteAccess uses the default `admin` theme). ### Use cases #### Note box You can create a custom style that places a paragraph in a note box: ![Example of a note box custom style](https://doc.ibexa.co/en/saas/content_management/img/oe_custom_style_note_box.png) ```yaml ibexa: system: admin_group: fieldtypes: ibexa_richtext: custom_styles: [note_box] ibexa_fieldtype_richtext: custom_styles: note_box: template: field_type/ibexa_richtext/custom_styles/note_box.html.twig ``` The `note_box.html.twig` template wraps the content of the selected text (`{{ content }}`) in a custom CSS class: ```html+twig
    {{ content }}
    ``` You can now define the custom CSS for this template, for example by using [Webpack Encore and assets](https://doc.ibexa.co/en/saas/templating/assets/index.md): ```css .note { display: block; background-color: #faa015; border-left: solid 5px #353535; line-height: 18px; padding: 15px; color: #fff; font-weight: bold; } ``` Add label for the new style by providing a translation in `translations/custom_styles.en.yaml`: ```yaml ibexa_richtext.custom_styles.note_box.label: 'Note box' ``` ![Adding a Note box custom style](https://doc.ibexa.co/en/saas/content_management/img/oe_custom_style_note_box_select.png) > **Tip: Tip** > > You can also create a similar note box with [custom classes](#note-box_1). #### Text highlight You can create an inline custom style that highlights a part of a text: ![Example of a custom style highlighting a portion of text](https://doc.ibexa.co/en/saas/content_management/img/oe_custom_style_highlight.png) ```yaml ibexa: system: admin_group: fieldtypes: ibexa_richtext: custom_styles: [highlight] ibexa_fieldtype_richtext: custom_styles: highlight: template: field_type/ibexa_richtext/custom_styles/highlight.html.twig inline: true ``` The `highlight.html.twig` template wraps the content of the selected text (`{{ content }}`) in a custom CSS class: ```html+twig {{ content }} ``` You can now define the custom CSS for this template, for example by using [Webpack Encore and assets](https://doc.ibexa.co/en/saas/templating/assets/index.md): ```css .highlight { background-color: #fcc672; border-radius: 25% 40% 25% 40%; } ``` Add label for the new style by providing a translation in `translations/custom_styles.en.yaml`: ```yaml ibexa_richtext.custom_styles.highlight.label: 'Highlight' ``` ![Adding a Highlight custom style](https://doc.ibexa.co/en/saas/content_management/img/oe_custom_style_highlight_select.png) ## Configure custom data attributes and classes You can add custom data attributes and CSS classes to the following elements in the Online Editor: - `embedInline` - `embed` - `formatted` - `heading` - `heading1` to `heading6` - `embedImage` - `ul` - `ol` - `li` - `paragraph` - `table` - `tr` - `td` - `link` > **Note: Heading elements** > > `heading` applies to all heading elements, and `heading1` to `heading6` to specific heading levels. > > When you configure both `heading` and a specific heading level (for example, `heading2`) at the same time, only the more specific configuration applies, in this case, `heading2`. > **Caution: Overriding embed templates** > > If you override the default templates for `embedInline`, `embed` or `embedImage` elements, for example, `@IbexaCore/default/content/embed.html.twig`, the data attributes and classes aren't rendered automatically. > > Instead, you can make use of the `data_attributes` and `class` properties in your templates. With the `ibexa_data_attributes_serialize` helper you can serialize the data attribute array. ### Custom data attributes You configure custom data attributes under the `fieldtypes.ibexa_fieldtype_richtext.attributes` key. The configuration is SiteAccess-aware. A custom data attribute can belong to one of the following types: `choice`, `boolean`, `string`, or `number`. You can also set each attribute to be `required` and set its `default_value`. For the `choice` type, you must provide an array of available `choices`. By adding `multiple`, you can decide whether more than one option can be selected. It's set to `false` by default. Use the example below to add two data attributes, `custom_attribute` and `another_attribute` to the Heading element in the `admin_group` SiteAccess: ```yaml ibexa: system: admin_group: fieldtypes: ibexa_richtext: attributes: heading: custom-attribute: type: boolean default_value: false another-attribute: type: choice choices: [attr1, attr2] default_value: attr2 required: false multiple: true ``` The configuration outputs `data-ezattribute-=""` in the corresponding HTML element. Here, the resulting values are `data-ezattribute-custom-attribute="false"` and `data-ezattribute-another-attribute="attr1,attr2"`. ### Custom CSS classes You configure custom CSS classes under the `fieldtypes.ibexa_richtext.classes` key. The configuration is SiteAccess-aware. You must provide the available `choices`. You can also set the values for `required`, `default_value` and `multiple`. `multiple` is set to true by default. Use the example below to add a class choice to the Paragraph element in the `admin_group` SiteAccess: ```yaml ibexa: system: admin_group: fieldtypes: ibexa_richtext: classes: paragraph: choices: [regular, special, tip_box, warning_box] default_value: regular required: false multiple: false ``` > **Note: Label translations** > > If there are many custom attributes, to provide label translations for these attributes, you can use the `ez_online_editor_attributes` translation extractor to get a full list of all custom attributes for all elements in all scopes. > > For example: > > ```bash > php ./bin/console jms:translation:extract --enable-extractor=ez_online_editor_attributes \ > --dir=./templates --output-dir=./translations/ --output-format=yaml > ``` ### Use cases #### Note box You can create a custom class that enables you to place a paragraph element in a note box: ![Example of a note box custom style](https://doc.ibexa.co/en/saas/content_management/img/oe_custom_style_note_box.png) ```yaml ibexa: system: admin_group: fieldtypes: ibexa_richtext: classes: paragraph: choices: [regular, special, tip_box, warning_box] ``` With this class you can choose one of the following classes for each paragraph element: `regular`, `tip_box`, or `warning_box`. You can then style the class by using CSS. ![Selecting a custom style for a paragraph](https://doc.ibexa.co/en/saas/content_management/img/oe_custom_class_note_box_select.png) > **Tip: Tip** > > You can also create a similar note box with [custom styles](#note-box). ## Rearrange buttons You can modify the order and visibility of buttons that are available in the Online Editor toolbar through configuration: ```yaml ibexa: system: admin_group: fieldtypes: ibexa_richtext: custom_tags: [ezyoutube, eztwitter, ezfacebook] toolbar: group1: priority: 60 buttons: ibexaMoveUp: priority: 30 ibexaMoveDown: priority: 20 heading: priority: 10 group2: priority: 50 buttons: alignment: priority: 10 ``` For each button you can set `priority`, which defines the order of buttons in the toolbar. For a full list of standard buttons, see the RichText module's [configuration file](https://github.com/ibexa/fieldtype-richtext/blob/6.0/src/bundle/Resources/config/prepend/ezpublish.yaml) ## Add CKEditor plugins Regular CKEditor plugins can be added to the Online Editor. This procedure is illustrated with the addition of the [Special characters plugin](https://ckeditor.com/docs/ckeditor5/latest/features/special-characters.html). You can install a CKEditor plugin locally by using `yarn add` or `npm install`, and deploy it by committing the `yarn.lock` file. A local installation looks like: ```bash yarn add @ckeditor/ckeditor5-special-characters@40.2.0 ``` Make sure to specify a version range compatible with the CKEditor's version used in Cohesivo. The CKEditor plugin must be added to the `ibexa.richText.CKEditor.extraPlugins` array. For this purpose, create an `assets/js/richtext.ckeditor-plugins.js` to import the plugin elements and add them to the array using `ibexa.addConfig` : ```js // The plugin itself import SpecialCharacters from '../../node_modules/@ckeditor/ckeditor5-special-characters/src/specialcharacters'; // The character list that will be used by the plugin import SpecialCharactersEssentials from '../../node_modules/@ckeditor/ckeditor5-special-characters/src/specialcharactersessentials'; ibexa.addConfig('richText.CKEditor.extraPlugins', [ SpecialCharacters, SpecialCharactersEssentials ], true); ``` The plugin is imported from `../../node_modules/@ckeditor` path and not directly from `@ckeditor` alias because this alias points at `./public/bundles/ibexaadminuiassets/vendors/@ckeditor`. Add the previous file to `ibexa-richtext-onlineeditor-js` Webpack Encore entry. Create the following `encore/ibexa.richtext.config.manager.js` file: ```js const path = require('path'); module.exports = (ibexaConfig, ibexaConfigManager) => { ibexaConfigManager.add({ ibexaConfig, entryName: 'ibexa-richtext-onlineeditor-js', newItems: [path.resolve(__dirname, '../assets/js/richtext.ckeditor-plugins.js')], }); }; ``` See [Importing assets from a bundle](https://doc.ibexa.co/en/saas/administration/back_office/back_office_elements/importing_assets_from_bundle/index.md) for alternative ways to add files to Webpack Encore entries. Add the plugin button to the RichText toolbar config (under `ibexa.system..fieldtypes.ibexa_richtext.toolbar`). A new button group is defined in `config/packages/ibexa_admin_ui.yaml` with [the `specialcharacters` button exposed by the plugin API](https://ckeditor.com/docs/ckeditor5/latest/features/special-characters.html#common-api): ```yaml ibexa: # … system: admin_group: # … fieldtypes: ibexa_richtext: toolbar: my_group: priority: 25 buttons: specialCharacters: priority: 10 ``` Build the assets and clear the cache by running `composer run-script auto-scripts`. For more information, see [CKEditor plugins documentation](https://ckeditor.com/docs/ckeditor5/latest/framework/architecture/plugins.html). ## Change CKEditor configuration You can add or override CKEditor configuration to set one of the [available properties](https://ckeditor.com/docs/ckeditor5/latest/api/module_core_editor_editorconfig-EditorConfig.html). To do it, add a custom config object to the `window.ibexa.richText.CKEditor.extraConfig` key by using the `addConfig` method: ```js window.ibexa.addConfig('richText.CKEditor.extraConfig', {your_custom_config_object}, true); ``` To have `Arrows` category from [previously added Special characters plugin](#add-ckeditor-plugins) on [top of the filter menu](https://ckeditor.com/docs/ckeditor5/latest/features/special-characters.html#ordering-categories): ```js ibexa.addConfig('richText.CKEditor.extraConfig', { specialCharacters: { order: ['Arrows'] } }, true); ``` ![CKEditor Special characters: Arrows category on top of the character filter](https://doc.ibexa.co/en/saas/content_management/img/ckeditor-special-characters_arrows-on-top.png) You can also use custom functions to modify the plugin configuration. The following example adds two ways to add a non-breaking space character: ```js function SpecialCharactersNbsp( editor ) { // add non-breaking space to the SpecialCharacters plugin editor.plugins.get( 'SpecialCharacters' ).addItems( 'Text', [ { title: 'Non-Breaking Space', character: '\u00a0' } ] ); // add a keyboard shortcut editor.keystrokes.set( 'Ctrl+space', ( key, stop ) => { editor.execute( 'input', { text: '\u00a0' } ); stop(); } ); } ibexa.addConfig('richText.CKEditor.extraPlugins', [ SpecialCharacters, SpecialCharactersEssentials, SpecialCharactersNbsp ], true); ``` # Create custom RichText block > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). Create a custom Page block containing rich text. Editions: Experience A RichText block is a specific example of a [custom block](https://doc.ibexa.co/en/saas/content_management/pages/create_custom_page_block/index.md) that you can use when you create a page. To create a custom block, you must define the block's layout, provide templates, add a subscriber, and register the subscriber as a service. Follow the procedure below to create a RichText page block. First, provide the block configuration under the `ibexa_page_fieldtype.blocks` [configuration key](https://doc.ibexa.co/en/saas/administration/configuration/configuration/#configuration-files). The following code defines a new block, its view and configuration templates. It also sets the attribute type to `richtext` (line 15): ```yaml ibexa_fieldtype_page: blocks: my_block: name: My Richtext Block thumbnail: assets/images/blocks/richtext_block_icon.svg configuration_template: '@ibexadesign/blocks/my_block/config.html.twig' views: default: template: '@ibexadesign/blocks/my_block/default.html.twig' name: My block view priority: -255 attributes: content: name: Content type: richtext ``` > **Note: Note** > > Make sure that you provide an icon for the block in the `assets/images/blocks/` folder. Then, create a subscriber that converts a string of data into XML code. Create a `src/Event/Subscriber/RichTextBlockSubscriber.php` file. In line 28, `my_block` is the same name of the block that you defined in line 3 above. Line 28 links this block `PreRenderEvent` to a method using `BlockRenderEvents::getBlockPreRenderEventName()`. Lines 37-47 handle the conversion of content into an XML string: ```php 'onBlockPreRender', ]; } /** * @param \Ibexa\FieldTypePage\FieldType\Page\Block\Renderer\Event\PreRenderEvent $event */ public function onBlockPreRender(PreRenderEvent $event): void { $renderRequest = $event->getRenderRequest(); if (!$renderRequest instanceof TwigRenderRequest) { return; } $parameters = $renderRequest->getParameters(); $parameters['document'] = null; $xml = $event->getBlockValue()->getAttribute('content')->getValue(); if (!empty($xml)) { $parameters['document'] = $this->domDocumentFactory->loadXMLString($xml); } $renderRequest->setParameters($parameters); } } ``` Now you can create [templates](https://doc.ibexa.co/en/saas/templating/templates/templates/index.md) that are used for displaying and configuring your block. Create the view template in `templates/themes//blocks/my_block/richtext.html.twig`. Line 2 is responsible for rendering the content from XML to HTML5: ```html+twig
    {{ document | ibexa_richtext_to_html5 }}
    ``` Then, create a separate `templates/themes/admin/blocks/my_block/config.html.twig` template: ```html+twig {% extends '@IbexaPageBuilder/page_builder/block/config.html.twig' %} {% block meta %} {{ parent() }} {% endblock %} ``` Finally, register the subscriber as a service in `config/services.yaml`: ```yaml services: App\Event\Subscriber\RichTextBlockSubscriber: tags: - { name: kernel.event_subscriber } ``` You have successfully created a custom RichText block. You can now add your block in the **Site** tab. ![RichText block](https://doc.ibexa.co/en/saas/content_management/img/extending_richtext_block.png) For more information about customizing additional options of the block or creating custom blocks with other attribute types, see [Create custom Page block](https://doc.ibexa.co/en/saas/content_management/pages/create_custom_page_block/index.md). # File management > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). Configurations and management of binary files. ## Access binary files To access binary files from the PHP API, use the `Ibexa\Core\IO\IOServiceInterface::loadBinaryFile()` method: ```php /** * @var \Ibexa\Contracts\Core\Repository\Values\Content\Field $field * @var \Ibexa\Core\IO\IOServiceInterface $ioService */ $file = $ioService->loadBinaryFile($field->value->id); $fileContent = $ioService->getFileContents($file); ``` ## Handling binary files Cohesivo supports multiple binary file handling mechanisms by means of an `IOHandler` interface. This feature is used by the [BinaryFile](https://doc.ibexa.co/en/saas/content_management/field_types/field_type_reference/imagefield/index.md) field types. ### Native IO handler The IO API is organized around two types of handlers, both used by the IOService: - `Ibexa\Core\IO\IOMetadataHandler`: stores and reads metadata (such as validity or size) - `Ibexa\Core\IO\IOBinarydataHandler`: stores and reads the actual binary data You can configure IO handlers using semantic configuration. IO handlers are configurable per SiteAccess. See the default configuration: ```yaml ibexa: system: default: io: metadata_handler: dfs binarydata_handler: nfs ``` The adapter is the *driver* used by Flysystem v2 to read/write files. Adapters are declared using `oneup_flysystem`. Metadata and binary data handlers are configured under `ibexa_io`. See below the configuration for the default handlers. It declares a metadata handler and a binary data handler, both labeled `default`. Both handlers are of type `flysystem`, and use the same Flysystem v2 adapter, labeled `default` as well. ```yaml ibexa_io: binarydata_handlers: nfs: flysystem: adapter: nfs_adapter metadata_handlers: dfs: legacy_dfs_cluster: connection: doctrine.dbal.dfs_connection ``` The `nfs_adapter`'s directory is based on your site settings, and is automatically set to `$var_dir$/$storage_dir$` (for example, `/path/to/ibexa/public/var/site/storage`). #### Permissions of generated files You can configure permissions of generated files under the `ibexa.system..io.permissions` [configuration key](https://doc.ibexa.co/en/saas/administration/configuration/configuration/#configuration-files). ```yaml ibexa: system: default: io: permissions: files: 0750 #default is 0644 directories: 0640 #default is 0755 ``` Both `files` and `directories` are optional. Default values: - 0644 for files - 0755 for directories > **Note: Note** > > Make sure to configure permissions using a number and **not** a string. "0644" is **not** interpreted by PHP as an octal number, and unexpected permissions can be applied. > **Note: Note** > > As SiteAccess configuration Flysystem's v2 native Local NFS adapter isn't supported, the following configuration should be used: > > ```yaml > oneup_flysystem: > adapters: > nfs_adapter: > custom: > service: ibexa.io.nfs.adapter.site_access_aware > ``` ### Native Flysystem v2 handler Cohesivo uses it as the default way to read and write content in form of binary files. Flysystem v2 can use the `local` filesystem, but is also able to read/write to `sftp`, `zip` or cloud filesystems (`azure`, `rackspace`, `S3`). [league/flysystem](https://flysystem.thephpleague.com/docs/) (along with [FlysystemBundle](https://github.com/1up-lab/OneupFlysystemBundle/)) is an abstract file handling library. #### Handler options ##### Adapter To be able to rely on dynamic SiteAccess-aware paths, you need to use Ibexa custom `nfs_adapter`. A basic configuration might look like the following: ```yaml oneup_flysystem: adapters: nfs_adapter: custom: service: ibexa.io.nfs.adapter.site_access_aware ``` To learn how to configure other adapters, see the [bundle's online documentation](https://github.com/1up-lab/OneupFlysystemBundle/blob/main/doc/index.md#step3-configure-your-filesystems). > **Note: Note** > > Only the adapters are used here, not the filesystem configuration described in this documentation. ### DFS Cluster handler For clustering, the platform provides a custom metadata handler that stores metadata about your assets in the database. This is faster than accessing the remote NFS or S3 instance to read metadata. For more information, see [Clustering](https://doc.ibexa.co/en/saas/infrastructure_and_maintenance/clustering/clustering/index.md). # Binary and Media download > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). Create route to to enable binary and media files download. You can restrict files stored in BinaryFile or Media fields to certain user roles. These files aren't publicly downloadable from disk, and are instead served by a route that runs the necessary checks. This route is automatically generated as the `url` property for those field values. ## REST API: `uri` property The `uri` property of Binary fields in REST contains a valid download URL, prefixed with the same host as the REST Request. For [more information about REST API see the documentation](https://doc.ibexa.co/en/saas/api/rest_api/rest_api_usage/rest_api_usage/index.md). # File URL handling > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). Manage files URL. ## IO URL decoration By default, images and binary files that are referenced by the content are served from the same server as the application, for example `/var/site/storage/images/3/6/4/6/6463-1-eng-GB/kidding.png`. This is the default semantic configuration: ```yaml ibexa: system: default: io: url_prefix: '$var_dir$/$storage_dir$' ``` `$var_dir$` and `$storage_dir$` are dynamic, [SiteAccess-aware settings](https://doc.ibexa.co/en/saas/multisite/siteaccess/siteaccess_aware_configuration/index.md), and are replaced by their values in the execution context. ## Serving images with nginx One common use case is to use an optimized nginx to serve images in an optimized way. The previous example image could be made available as `http://static.example.com/var/site/storage/images/3/6/4/6/6463-1-eng-GB/kidding.png` by setting up a separate server that maps the `/path/to/ibexa/public/var` directory. The configuration would be as follows: ```yaml ibexa: system: default: io: url_prefix: 'https://static.example.com/$var_dir$/$storage_dir$' ``` > **Caution: Caution** > > For security reasons, don't map `/path/to/ibexa/public/` as Document Root of the static server. Map the `/var/` directory directly to `/path/to/ibexa/public/var` instead. ## `io.url_prefix` Any BinaryFile returned by the public PHP API is prefixed with the value of this setting, internally stored as `ibexa.site_access.config..io.url_prefix`. ### `io.url_prefix` dynamic service container setting Default value: `$var_dir$/$storage_dir$` Example: `/var/site/storage` You can use `io.url_prefix` to configure the default URL decorator service (`ibexa.core.io.default_url_decorator`), used by all binary data handlers to generate the URI of loaded files. It's always interpreted as an absolute URI, meaning that unless it contains a scheme (`http://`, `ftp://`), is prepended with a `/`. This setting is SiteAccess-aware. ### Services #### URL decorators A `Ibexa\Core\IO\UrlDecorator` decorates and undecorates a specified string (URL). It has two mirror methods: `decorate` and `undecorate`. Two implementations are provided: `Prefix`, and `AbsolutePrefix`. They both add a prefix to a URL, but `AbsolutePrefix` ensures that unless the prefix is an external URL, the result is prepended with `/`. Three URL decorator services are introduced: - `Ibexa\Core\IO\UrlDecorator\AbsolutePrefix` used by the binary data handlers to decorate all URIs sent out by the API. Uses `AbsolutePrefix`. - `Ibexa\Core\IO\UrlDecorator\Prefix` used through the `UrlRedecorator` by various legacy elements (for example, converter or storage gateway) to generate its internal storage format for URIs. Uses a `Prefix`, not an `AbsolutePrefix`, meaning that no leading `/` is added. In addition, a URL redecorator service, `Ibexa\Core\IO\UrlDecorator\Prefix`, uses both previously mentioned decorators to convert URIs between what is used on the new stack, and what format legacy expects (relative URLs from the project root). # Pages > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). Pages are block-based special types of content that editors can create and modify by using a visual drag-and-drop editor. Editions: Experience Pages are block-based special types of content that editors can create and modify by using a visual drag-and-drop editor. - [Page Builder product guide](https://ez-systems-developer-documentation--3381.com.readthedocs.build/en/3381/content_management/pages/page_builder_guide/): Read about the Page Builder - a powerful tool for creating and modifying pages in Cohesivo. - [Page blocks](https://ez-systems-developer-documentation--3381.com.readthedocs.build/en/3381/content_management/pages/page_blocks/): Use blocks to customize the content of a Page with dynamic content. - [Page block attributes](https://ez-systems-developer-documentation--3381.com.readthedocs.build/en/3381/content_management/pages/page_block_attributes/): Page blocks can contain multiple attributes, of both built-in and custom types. - [Page block validators](https://ez-systems-developer-documentation--3381.com.readthedocs.build/en/3381/content_management/pages/page_block_validators/): Set up rules for validating Page block content. - [Create custom Page block](https://ez-systems-developer-documentation--3381.com.readthedocs.build/en/3381/content_management/pages/create_custom_page_block/): Create and configure custom Page blocks to add customized content to Pages. # Page Builder product guide > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). Read about the Page Builder - a powerful tool for creating and modifying pages in Cohesivo. Editions: Experience ## What is page [Page](https://doc.ibexa.co/en/saas/content_management/pages/pages/index.md) is a block-based type of content. You can create and modify it with a visual drag-and-drop editor - Page Builder. Page is divided into zones into which you can drop various dynamic blocks. By editing pages you can customize the layout and content of your website. ### Create page To create a new page: 1. In the main menu, go to **Content**. 2. Select **Content structure**. 3. On the right-side toolbar, click **Create content**. 4. From the list of content items select **Landing Page**. 5. Select the layout and click **Create**. ![Create page](https://doc.ibexa.co/en/saas/content_management/img/create_page.png) ### Edit page You can edit any existing page with the Page Builder. To do it, in the back office go to **Content** and select **Content structure**. Then, from the content tree choose the page and click **Edit**. ## What is Page Builder Page Builder is a visual tool that allows you to create and edit any page in Cohesivo. It's more than managing: it's about building pages, creating customized content and fully-targeted landing pages. Creating pages in Page Builder involves composing content from ready-to-use elements - blocks, properly configured and customized. It's also important to choose a layout - it determines the arrangement of drop zones that contain content elements. ![Page Builder - diagram](https://doc.ibexa.co/en/saas/content_management/img/page_builder_diagram.png) ### Availability Page Builder is available in Ibexa Experience and Ibexa Commerce. ### How does Page Builder work #### Page Builder interface Page Builder has plain and intuitive interface. You can create a Page without having advanced technical skills. ![Page Builder interface](https://doc.ibexa.co/en/saas/content_management/img/page_builder_interface.png) Page Builder user interface consists of: A. Drop zone B. Page blocks / Structure view toolbar C. Settings toolbar (including Fields, Visibility and Schedule settings) D. Mode toolbar (including PC, tablet and mobile mode) E. Buttons: | Button | Description | | ----------------------- | --------------------------------------------------------------------------------------------------------------------------- | | Edit and preview switch | Access main properties of the page, like title and description. | | Preview segments | Access preview of the page for a given segment. | | Timeline button | Access the timeline to preview how the page changes with time. You can also view the list of all upcoming scheduled events. | | View toggler | Toggle through to see how the page is rendered on different devices. | | Page blocks menu | Move Page blocks / Structure view to the other side of the screen. | | Undo | Undo latest change. | | Redo | Redo latest change. | F. Saving options | Option | Description | | ----------------------- | -------------------------------------------------- | | Close | Close the page without saving it. | | Send to review | Save the page and send it to review. | | Publish / Publish later | Publish the page or schedule publishing for later. | | Save draft | Save the page draft\*. | | Delete draft | Delete the page draft. | \*To help you preserve your work, system saves drafts of content items automatically. For more information, see [Autosave](https://doc.ibexa.co/projects/userguide/en/6.0/content_management/content_versions/#autosave). Page Builder has two main views that you can use while creating a page: - Page blocks toolbar - consists of all available elements that you can use by dragging them and dropping on a drop zone. ![Page blocks](https://doc.ibexa.co/en/saas/content_management/img/page_blocks_toolbar.png) - Structure view - shows a structure of the page, including its division into zones and the blocks that it contains. It follows the behavior of the content tree. Structure view has ability to reorder blocks using drag and drop. ![Structure view](https://doc.ibexa.co/en/saas/content_management/img/structure_view.png) ##### Choose layout For newly created Page you can choose a [layout](https://doc.ibexa.co/projects/userguide/en/6.0/content_management/configure_ct_field_settings/#available-page-layouts) which defines the available zones. Applying a layout divides the Page into the defined zones. The zones are placeholders for content items. On the Page creation modal, select the layout and click **Create draft**. Now you're ready to add blocks of content to the Page. The page layouts that an editor has access to are up to you to choose. In the `Select layouts` section, you can select layouts that you want to be available for the Page. ![Switch layout](https://doc.ibexa.co/en/saas/content_management/img/switch_layout_window.png) The default, built-in Page layout has only one zone, but developers can create other layouts in configuration. For more information, see [Configure layout](https://doc.ibexa.co/en/saas/templating/render_content/render_page/#configure-layout). #### Add blocks To customize your page in Page Builder you need to add blocks. To do it, access Page blocks toolbar, drag page block that you want to use, and drop it on the empty place on a drop zone. When you add a new block to the drop zone, drop it in the blue highlighted area. Before you drop it, a bold line appears - it helps you see the position of the newly added block in relation to other, already added blocks. ![Drop zone line](https://doc.ibexa.co/en/saas/content_management/img/drop_zone_line.png) Ready-to-use blocks available in Cohesivo have their own, unique functions, but you can also [add your own, custom blocks](https://doc.ibexa.co/en/saas/content_management/pages/create_custom_page_block/index.md). All available tools and settings, that Page Builder comes with, enable you to customize the content appearing on the page. You can check all ready-to-use blocks available in Page Builder in User Documentation, [Block reference page](https://doc.ibexa.co/projects/userguide/en/6.0/content_management/block_reference/). #### Work with blocks Working with blocks is intuitive. You don't have to worry about placing blocks in the proper place from the start - you can reorder them at any time. You can reorder blocks in a few ways: - drag and drop block in the desired location on a drop zone - select block and use up and down arrow on the keyboard - access Structure view and use 'Move up' and 'Move down' function in the settings of the block or drag and drop to change the position in the structure ![Structure view - drag and drop](https://doc.ibexa.co/en/saas/content_management/img/structure_view_drag_drop.png) You can manage each block by accessing its settings. To do it, click settings icon next to the block's name. ![Block settings](https://doc.ibexa.co/en/saas/content_management/img/block_settings.png) Available settings are: - Move up - allows you to change position of the block on the page by moving it up - Move down - allows you to change position of the block on the page by moving it down - Configuration - allows you to access configuration window - Duplicate - duplicates a block with its settings, by creating a copy of it that appears below the original block - Refresh - refreshes preview of the block - Delete - deletes existing block #### Distraction free mode While configuring blocks that include Rich Text section, for example, Text block, you can switch to distraction free mode that expands the workspace to full screen. ![Distraction free mode](https://doc.ibexa.co/en/saas/content_management/img/distraction_free_mode.png) For more information, see [Distraction free mode](https://doc.ibexa.co/projects/userguide/en/6.0/content_management/create_edit_content_items/#distraction-free-mode). #### Schedule content Page Builder comes with a Scheduler, it allows you to schedule content appearance. You can schedule content to be revealed, or hidden in Page Builder in two ways with: - **Scheduler tab** - it's available in the configuration of all Page blocks. In this tab you can set the date and time when the block becomes visible and when it disappears from a Page. ![Scheduler tab](https://doc.ibexa.co/en/saas/content_management/img/scheduler_tab.png) - **Content Scheduler** - it's one of the blocks available in Page Builder Page blocks menu. To proceed with the schedule, go to **Basic** tab of the block, then click **Select content** and confirm your choice. Then set date and time in the **Content airtime settings** window. ![Content Scheduler](https://doc.ibexa.co/en/saas/content_management/img/content_scheduler.png) For more information, see [Schedule publication](https://doc.ibexa.co/projects/userguide/en/6.0/content_management/schedule_publishing/). ## Benefits ### Manage your pages without technical skills Thanks to intuitive and plain Page Builder interface, you can create and manage your website without the need of having advanced technical skills. Page blocks toolbar, visible page zones and Structure view - these are the elements that make working with Page Builder really intuitive and quick. ### Self schedule content, special offers and campaigns One of the most important tools that Page Builder offers, is a Scheduler. It allows you to set and schedule a specific date and time for the content to be published or hidden. As a result, you can manage timeline of publications, without the need of manual publishing, or hiding each of them. ### Create high-converting and fully-targeted landing pages Page Builder allows you to create highly customizable websites. You can build modifiable and targeted landing pages that meet your needs. Each dynamic blocks has its own settings, properties and design that you can set up in your way to customize the content appearing on the page. Additionally, if you feel comfortable with your technical skills, you can configure your own elements, for example, a new customized layout, or block. ### Increase sales with highly personalized campaigns Personalized campaigns are one of the factors that can increase your sales. With Page Builder you can achieve it, by using customization and time Scheduler. Anytime you can edit your page and change a position of a block to enhance visibility. Additionally, Page Builder offers you a selection of ready-to-use page blocks that can help you to create content tailored to each individual customer: A. **Default** blocks: - Targeting - embeds a content item based on the segment the user belongs to. B. **PIM** blocks: - Catalog - displays products from a specific catalog to a selected customer group. - Product collection - displays a list of specifically selected products. - Product embed - displays a specific product. C. [**Recommendations** blocks](https://doc.ibexa.co/en/saas/recommendations/raptor_integration/recommendation_blocks/index.md) - presents content recommendations delivered by Raptor integration. # Page blocks > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). Use blocks to customize the content of a Page with dynamic content. Editions: Experience Page blocks are configured in YAML files, under the `ibexa_fieldtype_page` key. Keep in mind that Page block configuration isn't SiteAccess-aware. Cohesivo ships with a number of page blocks. For a list of all page blocks that are available out-of-the-box, see [Page block reference](https://doc.ibexa.co/projects/userguide/en/6.0/content_management/block_reference/). For information on how to create and configure new layouts for the Page, see [Page layouts](https://doc.ibexa.co/en/saas/templating/render_content/render_page/#render-a-layout). > **Caution: Clear the persistence cache** > > Persistence cache must be cleared after any modifications have been made to the block config in Page Builder, such as adding, removing or altering the page blocks, block attributes, validators or views configuration. > > To clear the persistence cache, run `php bin/console cache:pool:clear ` command. The default cache pool is named `cache.tagaware.filesystem`. The default cache pool when running Redis or Valkey is named `cache.redis`. If you have customized the [persistence cache configuration](https://doc.ibexa.co/en/saas/infrastructure_and_maintenance/cache/persistence_cache/#what-is-cached), the name of your cache pool might be different. > > In prod mode, you also need to clear the symfony cache by running `./bin/console c:c`. In dev mode, the Symfony cache is rebuilt automatically. ## Block configuration Each configured block has an identifier and the following settings: | Setting | Description | | ------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `name` | Name of the block used in the Page Builder interface. Translatable using the `ibexa_page_fieldtype` translation domain. Also accepts a [`help` key](#block-name-and-help-text) that adds a helper text under the **Name** field in the block configuration form. | | `category` | Category in the Page Builder **Page blocks** toolbox that the block is shown in. Translatable using the `ibexa_page_fieldtype` translation domain. | | `thumbnail` | Thumbnail used in the Page Builder **Page blocks** toolbox. | | `views` | Available [templates for the block](#block-templates). | | `visible` | (Optional) Toggles the block's visibility in the Page Builder **Page blocks** toolbox. Remove the block from the layout before you publish another version of the page. | | `configuration_template` | (Optional) Template for the block settings modal. | | `attributes` | (Optional) List of [block attributes](https://doc.ibexa.co/en/saas/content_management/pages/page_block_attributes/index.md). | | `cacheable_query_params` | (Optional) List of query parameters the block's [ESI HTTP cache](https://doc.ibexa.co/en/saas/infrastructure_and_maintenance/cache/http_cache/http_cache_configuration/#when-to-use-esi) varies on. For example, if the block is paginated using `?page=ℕ` from the page URL, add `page` to this list. See the `ibexa_append_cacheable_query_params()` Twig function. | For example: ```yaml ibexa_fieldtype_page: blocks: event: name: event_block.name category: custom_category.name thumbnail: /bundles/ibexaadminuiassets/vendors/ids-assets/dist/img/all-icons.svg#calendar configuration_template: '@ibexadesign/blocks/event/config.html.twig' views: default: template: '@ibexadesign/blocks/event/template.html.twig' name: event_block.view.default priority: -255 attributes: # ... ``` > **Tip: Tip** > > For a full example of block configuration, see [Create custom Page block](https://doc.ibexa.co/en/saas/content_management/pages/create_custom_page_block/index.md). ### Block name and help text The `name` setting accepts either a single translation key, a hard coded string of text that won't be translated, or an object with `text` and `help` property keys. Both `text` and `help` are translatable using the `ibexa_page_fieldtype` translation domain. Scalar form: ```yaml ibexa_fieldtype_page: blocks: my_block: name: my_block.name.key ``` Structured form with a helper text: ```yaml ibexa_fieldtype_page: blocks: my_block: name: text: my_block.name.key help: my_block.name.help.key ``` - `text` - corresponds to the block name. - `help` - is an optional translation key whose translation is rendered as a helper text under the **Name** field in the block configuration form. ![Help text](https://doc.ibexa.co/en/saas/content_management/img/help_text.png) The same format is available for [React App blocks](https://doc.ibexa.co/en/saas/content_management/pages/react_app_block/index.md). ### Overwriting existing blocks You can overwrite the following properties in the existing blocks: - `name` - `category` - `thumbnail` - `views` ## Block templates Page blocks can have multiple templates. This allows you to create different styles for each block and let the editor choose them when adding the block from the UI. They names are translatable using the `ibexa_page_builder_block_config` translation domain. ```yaml ibexa_fieldtype_page: blocks: event: views: default: template: '@ibexadesign/blocks/event/template.html.twig' name: event_block.view.default priority: -255 featured: template: '@ibexadesign/blocks/event/featured_template.html.twig' name: event_block.view.featured priority: 50 ``` `priority` defines the order of block views on the block configuration screen. The highest number shows first on the list. > **Tip: Tip** > > Default views have a `priority` of -255. It's good practice to keep the value between -255 and 255. ### Block modal template The template for the configuration modal of built-in Page blocks is contained in `vendor/ibexa/page-builder/src/bundle/Resources/views/page_builder/block/config.html.twig`. You can override it by using the `configuration_template` [configuration key](https://doc.ibexa.co/en/saas/administration/configuration/configuration/#configuration-files): ```yaml ibexa_fieldtype_page: blocks: event: name: event_block.name category: custom_category.name thumbnail: /bundles/ibexaadminuiassets/vendors/ids-assets/dist/img/all-icons.svg#calendar configuration_template: '@ibexadesign/blocks/event/config.html.twig' ``` The template can extend the default `config.html.twig` and modify its blocks. Blocks `basic_tab_content` and `design_tab_content` correspond to the **Basic** and **Design** tabs in the modal. The following example wraps all form fields for block attributes in an ordered list: ```html+twig {% extends '@IbexaPageBuilder/page_builder/block/config.html.twig' %} {% block basic_tab_content %}
    {{ form_row(form.name) }} {% if attributes_per_category['default'] is defined %}
      {% for identifier in attributes_per_category['default'] %} {% block config_entry %}
    1. {{ form_row(form.attributes[identifier]) }}
    2. {% endblock %} {% endfor %}
    {% endif %}
    {% endblock %} ``` ## Block events To add functionalities to your block that go beyond the available attributes, you can use an event listener. You can listen to events related to block definition and block rendering. The following events are available: - `BlockDefinitionEvents::getBlockDefinitionEventName` - dispatched when block definition is created - `BlockDefinitionEvents::getBlockAttributeDefinitionEventName` - dispatched when block attribute definition is created - `BlockRenderEvents::getBlockPreRenderEventName` - dispatched before a block is rendered - `BlockRenderEvents::getBlockPostRenderEventName` - dispatched after a block is rendered For example, to modify a block by adding a new parameter to it, you can create the following listener: ```php 'onBlockPreRender', ]; } public function onBlockPreRender(PreRenderEvent $event): void { /** @var \Ibexa\FieldTypePage\FieldType\Page\Block\Renderer\Twig\TwigRenderRequest $renderRequest */ $renderRequest = $event->getRenderRequest(); $parameters = $event->getRenderRequest()->getParameters(); $parameters['my_parameter'] = 'parameter_value'; $renderRequest->setParameters($parameters); } } ``` Before the block is rendered, the listener adds `my_parameter` to it with value `parameter_value`. You can use this parameter, for example, in block template: ```html+twig
    {{ my_parameter }}
    ``` ### Exposing content relations from blocks Page blocks, for example Embed block or Collection block, can embed other content items. Publishing a page with such blocks creates Relations to those content items. When creating a custom block with embeds, you can ensure such Relations are created using the block Relation collection event. The event is dispatched on content publication. You can hook your event listener to the `BlockRelationEvents::getCollectBlockRelationsEventName` event. To expose relations, pass an array containing Content IDs to the `Ibexa\FieldTypePage\Event\CollectBlockRelationsEvent::setRelations()` method. If embedded Content changes, old Relations are removed automatically. Providing Relations also invalidates HTTP cache for your block response in one of the related content items changes. # Page block attributes > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). Page blocks can contain multiple attributes, of both built-in and custom types. Editions: Experience A block has attributes that the editor fills in when adding the block to a Page. > **Caution: Clear the persistence cache** > > Persistence cache must be cleared after any modifications have been made to the block config in Page Builder, such as adding, removing or altering the page blocks, block attributes, validators or views configuration. > > To clear the persistence cache, run `php bin/console cache:pool:clear ` command. The default cache pool is named `cache.tagaware.filesystem`. The default cache pool when running Redis or Valkey is named `cache.redis`. If you have customized the [persistence cache configuration](https://doc.ibexa.co/en/saas/infrastructure_and_maintenance/cache/persistence_cache/#what-is-cached), the name of your cache pool might be different. > > In prod mode, you also need to clear the symfony cache by running `./bin/console c:c`. In dev mode, the Symfony cache is rebuilt automatically. Each block can have the following properties: | Attribute | Description | | ------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `type` | Attribute type. | | `name` | (Optional) The displayed name for the attribute. You can omit it, block identifier is then used as the name. Translatable using the `ibexa_page_builder_block_config` translation domain. | | `value` | (Optional) The default value for the attribute. | | `category` | (Optional) The tab where the attribute is displayed in the block edit modal. | | `validators` | (Optional) [Validators](https://doc.ibexa.co/en/saas/content_management/pages/page_block_validators/index.md) checking the attribute value. | | `options` | (Optional) Additional options, dependent on the attribute type. | ## Block attribute types The following attribute types are available: | Type | Description | Options | | --------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `integer` | Integer value | - | | `string` | String | - | | `url` | URL | - | | `text` | Text block | - | | `richtext` | Rich text block (see [creating RichText block](https://doc.ibexa.co/en/saas/content_management/rich_text/create_custom_richtext_block/index.md)) | - | | `embed` | Embedded content item | `udw_config_name`: name of the [Universal Discovery Widget's configuration](https://doc.ibexa.co/en/saas/administration/back_office/browser/browser/#add-new-configuration) | | `embedvideo` | Embedded content item | `udw_config_name`: name of the [Universal Discovery Widget's configuration](https://doc.ibexa.co/en/saas/administration/back_office/browser/browser/#add-new-configuration) | | `select` | Drop-down with options to select | - `choices` lists the available options in `label: value` form - `multiple`, when set to true, allows selecting more than one option | | `checkbox` | Checkbox | Selects available option if `value: true`. Checkbox appearance in block configuration forms [can be configured](#configure-checkbox-appearance) | | `multiple` | Checkbox(es) | `choices` lists the available options in `label: value` form. | | `radio` | Radio buttons | `choices` lists the available options in `label: value` form. | | `locationlist` | Location selection | `udw_config_name`: name of the [Universal Discovery Widget's configuration](https://doc.ibexa.co/en/saas/administration/back_office/browser/browser/#add-new-configuration) | | `contenttypelist` | List of content types | - | | `schedule_events`, `schedule_snapshots`, `schedule_initial_items`, `schedule_slots`, `schedule_loaded_snapshot` | Used in the Content Scheduler block | - | | `nested_attribute` | Defines a group of attributes in a block. | - `attributes` - a list of attributes in the group. The attributes in the group are [configured](#page-block-attributes) as regular attributes - `multiple`, when set to true. New groups are added dynamically with the **+ Add** button | When you define attributes, you can omit most keys as long as you use simple types that don't require additional options: ```yaml attributes: first_field: text second_field: string third_field: integer ``` The `embed`, `embedvideo`, and `locationlist` attribute types use the Universal Discovery Widget (UDW). When creating a block with these types you can use the `udw_config_name` option to configure the UDW behavior. See the [custom block example](https://doc.ibexa.co/en/saas/content_management/pages/create_custom_page_block/#configure-block) to learn more. ## Custom attribute types You can create custom attribute type to add to Page blocks. A custom attribute requires attribute type class, a mapper and a template. ### Block attribute type First, create the attribute type class. It can extend one of the types available in `fieldtype-page/src/lib/Form/Type/BlockAttribute/`. You can also use one of the [built-in Symfony types](https://symfony.com/doc/7.4/reference/forms/types.html), for example `AbstractType` for any custom type or `IntegerType` for numeric types. To define the type, create a `src/Block/Attribute/MyStringAttributeType.php` file: ```php create( 'value', MyStringAttributeType::class, [ 'constraints' => $constraints, ] ); } } ``` Then, add a new service definition for your mapper to `config/services.yaml`: ```yaml App\Block\Attribute\MyStringAttributeMapper: tags: - { name: ibexa.page_builder.form_type_attribute.mapper, alias: my_string } ``` ### Edit templates Next, configure a template for the attribute edit form by creating a `templates/themes/admin/custom_form_templates.html.twig` file: ```html+twig {% block my_string_attribute_widget %}

    My String

    {{ form_widget(form) }} {% endblock %} ``` Add the template to your configuration under the `system..page_builder_forms` [configuration key](https://doc.ibexa.co/en/saas/administration/configuration/configuration/#configuration-files): ```yaml ibexa: system: default: page_builder_forms: block_edit_form_templates: - { template: '@ibexadesign/custom_form_templates.html.twig', priority: 0 } ``` ### Custom attribute configuration Now, you can create a block containing your custom attribute: ```yaml ibexa_fieldtype_page: blocks: my_block: name: MyBlock category: default thumbnail: /bundles/ibexaadminuiassets/vendors/ids-assets/dist/img/all-icons.svg#edit views: default: name: Default block layout template: '@ibexadesign/blocks/my_block.html.twig' priority: -255 attributes: my_string_attribute: type: my_string name: MyString ``` ### Nested attribute configuration The `nested_attribute` attribute is used when you want to create a group of attributes. First, make sure you have configured the attributes you want to use in the group. Next, provide the configuration. See the example: ```yaml ibexa_fieldtype_page: blocks: block_name: category: default thumbnail: 'path/icons.svg' views: default: { name: 'Default block layout', template: 'template.html.twig', priority: -255 } attributes: group: name: Group name type: nested_attribute options: attributes: attribute_1: name: Name 1 type: string attribute_2: name: Name 2 type: string multiple: true ``` To set validation for each nested attribute: ```yaml name: Group name type: nested_attribute options: attributes: attribute_1: name: Name 1 type: string validators: not_blank: message: 'Provide a value' ``` Validators can be also set on a parent attribute (group defining level), it means all validators apply to each nested attribute: ```yaml name: Group name type: nested_attribute options: attributes: attribute_1: name: Name 1 type: string attribute_2: name: Name 2 type: string multiple: true validators: not_blank: message: 'Provide a value' ``` > **Caution: Moving attributes between groups** > > If you move an attribute between groups or add an ungrouped attribute to a group, the block values are removed. ## Help messages for form fields With the `help`, `help_attr`, and `help_html` field options, you can define help messages for fields in the Page block. You can set options with the following configuration: ```yaml ibexa_fieldtype_page: blocks: block_name: attributes: attribute_name: options: help: text: 'Some example text' html: true|false attr: class: 'class1 class2' ``` - `help.text` - defines a help message which is rendered below the field (maps to [`help`](https://symfony.com/doc/7.4/reference/forms/types/form.html#help)) - `help.attr` - sets the HTML attributes for the element which displays the help message (maps to [`help_attr`](https://symfony.com/doc/7.4/reference/forms/types/form.html#help-attr)) - `help.html` - enable (default) / disable (set to `true`) escaping the contents of the `help.text` option when rendering in the template (maps to [`help_html`](https://symfony.com/doc/7.4/reference/forms/types/form.html#help-html)) ### Help message in nested attributes You can set the options for root or nested attribute, see the example configuration: ```yaml ibexa_fieldtype_page: blocks: slider: category: default thumbnail: '/bundles/ibexaadminuiassets/vendors/ids-assets/dist/img/all-icons.svg#edit' views: default: { name: 'Default block layout', template: 'themes/blocks/slider.html.twig', priority: -255 } attributes: group: name: Group name type: nested_attribute options: help: text: 'Root class text' html: true # true|false attr: class: 'root-class-1 root-class-2' attributes: integer: name: Age type: integer validators: not_blank: message: 'Provide a value' options: help: text: 'Nested attribute text' html: true attr: class: 'nested-1 nested-2' string: name: Name type: string validators: not_blank: message: 'Provide a value' ``` ![Help message](https://doc.ibexa.co/en/saas/content_management/img/page_block_help_message.png "Help message") ## Configure checkbox appearance For blocks with an attribute of `checkbox` type, you can change the look of the checkbox in block configuration forms. You can do it by adding the `block_prefix: block_configuration_attribute_checkbox_toggle` option in the block configuration as follows: ```yaml : name: type: checkbox options: block_prefix: block_configuration_attribute_checkbox_toggle ``` This setting changes the checkbox appearance to a toggle widget. ![Toggle widget](https://doc.ibexa.co/en/saas/content_management/img/toggle_widget.png) If you remove the above setting from the configuration, the attribute reverts to the default checkbox appearance. # Page block validators > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). Set up rules for validating Page block content. Editions: Experience Validators check values passed to Page block attributes. The following block validators are available: - `required` - checks whether the attribute is provided - `regexp` - validates attribute according to the provided regular expression - `not_blank` - checks whether the attribute isn't left empty - `not_blank_richtext` - checks whether a `richtext` attribute isn't left empty - `content_type` - checks whether the selected content types match the provided values - `content_container` - checks whether the selected content item is a container > **Note: Note** > > Don't use the `required` and `not_blank` validators for `richtext` attributes. Instead, use `not_blank_richtext`. For each validator you can provide a message that displays in the Page Builder when an attribute field doesn't fulfill the criteria. Additionally, for some validators you can provide settings under the `ibexa_fieldtype_page.blocks..validators.regexp.options` [configuration key](https://doc.ibexa.co/en/saas/administration/configuration/configuration/#configuration-files), for example: ```yaml email: type: string name: E-mail address validators: regexp: options: pattern: '/^\S+@\S+\.\S+$/' message: Provide a valid e-mail address ``` ## Custom validators You can create Page block attributes with custom validators. The following example shows how to create a validator which requires that string attributes contain only alphanumeric characters. First, create classes that support your intended method of validation. For example, in `src/Validator`, create an `AlphaOnly.php` file: ```php context->buildViolation($constraint->message) ->setParameter('{{ string }}', $value) ->addViolation(); } } } ``` Then, under `ibexa_fieldtype_page.block_validators`, enable the new validator in Page Builder: ```yaml ibexa_fieldtype_page: block_validators: alpha_only: 'App\Validator\AlphaOnly' ``` Finally, add the validator to one of your block attributes, for example: ```yaml ibexa_fieldtype_page: blocks: my_block: name: My Block category: default thumbnail: /bundles/ibexaadminuiassets/vendors/ids-assets/dist/img/all-icons.svg#edit views: default: name: Default block layout template: '@ibexadesign/blocks/my_block.html.twig' priority: -255 attributes: my_text_attribute: type: text name: My text attribute validators: alpha_only: message: The field can only contain letters or numbers. ``` ### Custom required validator By default, only `not_blank` and `not_blank_richtext` validators mark a block attribute as required. If you create a custom validator `custom_not_blank` with attribute-specific logic, you can extend the `AttributeType` class with a Symfony form type extension to make sure that the attribute is also considered required: ```php getConstraints()['custom_not_blank'])) { $builder->setRequired(true); } } public static function getExtendedTypes(): iterable { return [ AttributeType::class, ]; } } ``` # Create custom Page block > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). Create and configure custom Page blocks to add customized content to Pages. Editions: Experience In addition to existing blocks which you can use in a Page, you can also create custom blocks. To do this, add block configuration in a YAML file, under the `ibexa_fieldtype_page` [configuration key](https://doc.ibexa.co/en/saas/administration/configuration/configuration/#configuration-files). > **Caution: Clear the persistence cache** > > Persistence cache must be cleared after any modifications have been made to the block config in Page Builder, such as adding, removing or altering the page blocks, block attributes, validators or views configuration. > > To clear the persistence cache, run `php bin/console cache:pool:clear ` command. The default cache pool is named `cache.tagaware.filesystem`. The default cache pool when running Redis or Valkey is named `cache.redis`. If you have customized the [persistence cache configuration](https://doc.ibexa.co/en/saas/infrastructure_and_maintenance/cache/persistence_cache/#what-is-cached), the name of your cache pool might be different. > > In prod mode, you also need to clear the symfony cache by running `./bin/console c:c`. In dev mode, the Symfony cache is rebuilt automatically. The following example shows how to create a block that showcases an event. ## Configure block First, add the following [YAML configuration](https://doc.ibexa.co/en/saas/administration/configuration/configuration/#configuration-files): ```yaml ibexa_fieldtype_page: blocks: event: name: event_block.name category: custom_category.name thumbnail: /bundles/ibexaadminuiassets/vendors/ids-assets/dist/img/all-icons.svg#calendar attributes: name: type: text name: event_block.name.name validators: not_blank: message: validators.message.event_block.name.validator.not_blank category: type: select name: event_block.category.name value: visual options: multiple: true choices: 'Music': music 'Visual arts': visual 'Sports': sports event: type: embed name: event_block.event.name options: udw_config_name: block_event_embed validators: not_blank: message: validators.message.event_block.embed.validator.not_blank content_type: message: validators.message.event_block.embed.validator.content_type options: types: ['event'] regexp: message: validators.message.event_block.embed.validator.content_item options: pattern: '/[0-9]+/' ``` And provide the translations for the labels: - in `translations/ibexa_page_builder_block_config.en.yaml`: ```yaml event_block.view.default: Default event_block.view.featured: Featured event_block.name.name: Name event_block.category.name: Category event_block.event.name: Event ``` - in `translations/ibexa_page_fieldtype.en.yaml`: ```yaml custom_category.name: Custom category event_block.name: Event ``` - in `translations/validators.en.yaml`: ```yaml validators.message.event_block.name.validator.not_blank: Event name should not be blank. validators.message.event_block.embed.validator.not_blank: Event content should not be blank. validators.message.event_block.embed.validator.content_type: Event content should be of type "event". validators.message.event_block.embed.validator.content_item: Event content should have a numerical ID. ``` `event` is the internal name for the block, and `name` indicates the name under which the block is available in the interface. You also set up the category in the **Page blocks** toolbox that the block appears in. In this case, it doesn't show up with the rest of the built-in blocks, but in a separate "Custom category" category. The thumbnail for the block can be one of the pre-existing icons, like in the example above, or you can use a custom SVG file. A block can have multiple attributes that you edit when adding it to a page. In this example, you configure three attributes: name of the event, category it belongs to, and an event content item that you select and embed. For a list of all available attribute types, see [Page block attributes](https://doc.ibexa.co/en/saas/content_management/pages/page_block_attributes/index.md). Each attribute can have [validators](https://doc.ibexa.co/en/saas/content_management/pages/page_block_validators/index.md). The `not_blank` validators in the example ensure that the user fills in the two block fields. The `content_type` validator in the example ensure that the user choose a content item of the content type `event`. The `regexp` validator ensure that the final value looks like a content ID. The following UDW configuration is used with the `udw_config_name` key so only an event typed content item can be selected: ```yaml ibexa: system: default: universal_discovery_widget_module: configuration: block_event_embed: multiple: false allowed_content_types: ['event'] ``` For more information, see [UDW configuration](https://doc.ibexa.co/en/saas/administration/back_office/browser/browser/#udw-configuration). ## Add block templates A block can have different templates that you select when adding it to a page. To configure block templates, add them to block configuration: ```yaml ibexa_fieldtype_page: blocks: event: views: default: template: '@ibexadesign/blocks/event/template.html.twig' name: event_block.view.default priority: -255 featured: template: '@ibexadesign/blocks/event/featured_template.html.twig' name: event_block.view.featured priority: 50 ``` Provide the templates in the indicated folder, in this case in `templates/themes//blocks/event`. For example the `featured_template.html.twig` file can look like this: ```html+twig

    {{ name }}

    {{ category }}

    {{ render(controller('ibexa_content::viewAction', { 'contentId': event, 'viewType': 'embed' })) }} ``` The templates have access to all block attributes, as you can see above in the `name`, `category` and `event` variables. Priority of templates indicates the order in which they're presented in Page Builder. The template with the greatest priority is used as the default one. ## Add block JavaScript If your block is animated with JavaScript, you may have to take precaution to keep it working when previewed in back office's Page Builder. If you use an event related to the page being loaded to trigger the initialisation of your custom block, a freshly added block doesn't work in the Page Builder preview. For example, the [`DOMContentLoaded`](https://developer.mozilla.org/en-US/docs/Web/API/Document/DOMContentLoaded_event) event isn't fired when a block is dragged into the page as the DOM is already loaded. The Page Builder fires `body` events that you can listen to initialize your block: - `ibexa-render-block-preview` event is fired when the page is loaded in the Page Builder, when a block is added, when a block is deleted, and when a block setting modification is submitted. - `ibexa-post-update-blocks-preview` event is fired when a block setting modification is submitted, this event has a `detail` property listing the reloaded modified block IDs and their configs. In the following code, the same `initCustomBlocks` function is attached to two event listeners. One listener to call the function when a page is loaded (as a regular front page or as a page edited in the Page Builder). The other one to call it when a block is added or configured in the Page Builder. This `initCustomBlocks` function finds the custom blocks to loop through them, initializes some JavaScript when the block isn't already initialized, and flag the block as initialized. For example, it could initialize carousel blocks with the addition of event listeners to navigation arrows, and the start of an automatic sliding. ```javascript document.addEventListener('DOMContentLoaded', function(event) { initCustomBlocks(); }); document.getElementsByTagName('body')[0].addEventListener('ibexa-render-block-preview', function(event) { initCustomBlocks(); }); ``` > **Note: Note** > > For the addition of your custom block's JS and CSS files, see [Assets](https://doc.ibexa.co/en/saas/templating/assets/index.md). > > If you consider using React JavaScript library, see [React App block](https://doc.ibexa.co/en/saas/content_management/pages/react_app_block/index.md). ## Add pre-render event listener If you need to compute variables to pass to the template, you can listen or subscribe to the block pre-render event. For example, the following event subscriber loads the `event` content item and passes it to the template as `event_content`: ```php 'onBlockPreRender', ]; } public function onBlockPreRender(PreRenderEvent $event): void { /** @var \Ibexa\FieldTypePage\FieldType\Page\Block\Renderer\Twig\TwigRenderRequest $renderRequest */ $renderRequest = $event->getRenderRequest(); $parameters = $event->getRenderRequest()->getParameters(); $parameters['event_content'] = $this->contentService->loadContent($parameters['event']); $renderRequest->setParameters($parameters); } } ``` The block view template could now use `ibexa_render(event_content, {'viewType': 'embed'})` instead of `render(controller('ibexa_content::viewAction', {'contentId': event, 'viewType': 'embed'}))`, other content Twig functions, or field Twig functions. For more information, see [Block events](https://doc.ibexa.co/en/saas/content_management/pages/page_blocks/#block-events). ## Add edit template You can also customize the template for the block settings modal. Do this under the `configuration_template` [configuration key](https://doc.ibexa.co/en/saas/administration/configuration/configuration/#configuration-files): ```yaml ibexa_fieldtype_page: blocks: event: name: event_block.name category: custom_category.name thumbnail: /bundles/ibexaadminuiassets/vendors/ids-assets/dist/img/all-icons.svg#calendar configuration_template: '@ibexadesign/blocks/event/config.html.twig' ``` Place the edit template in `templates/themes//blocks/event/config.html.twig`: ```html+twig {% extends '@IbexaPageBuilder/page_builder/block/config.html.twig' %} {% block basic_tab_content %}
    {{ form_row(form.name) }} {% if attributes_per_category['default'] is defined %}
      {% for identifier in attributes_per_category['default'] %} {% block config_entry %}
    1. {{ form_row(form.attributes[identifier]) }}
    2. {% endblock %} {% endfor %}
    {% endif %}
    {% endblock %} ``` Your custom page block is now registered in the system. > **Caution: Caution** > > To use the new block in Page Builder, add it to the list of available blocks in a given content type's settings. This can be done manually in [Page field settings](https://doc.ibexa.co/projects/userguide/en/6.0/content_management/configure_ct_field_settings/#block-display) or by using the migration action [`add_block_to_available_blocks`](https://doc.ibexa.co/en/saas/content_management/data_migration/data_migration_actions/#content-types). # React App block > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). Create a block that allows an editor to embed a preconfigured React component into a page. Editions: Experience React App block allows an editor to embed a preconfigured React application into a page. It's configured in YAML files, under the `ibexa_fieldtype_page` key. Page block configuration isn't SiteAccess-aware. Another element of React App Block is `\Ibexa\FieldTypePage\FieldType\Page\Block\Event\Listener\ReactBlock` Listener which adds component and props variables. It's common to all the blocks. > **Caution: Clear the persistence cache** > > Persistence cache must be cleared after any modifications have been made to the block config in Page Builder, such as adding, removing or altering the page blocks, block attributes, validators or views configuration. > > To clear the persistence cache, run `php bin/console cache:pool:clear ` command. The default cache pool is named `cache.tagaware.filesystem`. The default cache pool when running Redis or Valkey is named `cache.redis`. If you have customized the [persistence cache configuration](https://doc.ibexa.co/en/saas/infrastructure_and_maintenance/cache/persistence_cache/#what-is-cached), the name of your cache pool might be different. > > In prod mode, you also need to clear the symfony cache by running `./bin/console c:c`. In dev mode, the Symfony cache is rebuilt automatically. ## React App Block configuration React App blocks are regular [Page blocks](https://doc.ibexa.co/en/saas/content_management/pages/page_blocks/index.md) and can be configured on field definition level as any other block. File has exactly the same structure as regular YAML [block configuration](https://doc.ibexa.co/en/saas/content_management/pages/create_custom_page_block/#configure-block), except: - additional `component` attribute which binds Page Builder block with React App - `views` attribute is removed Each configured React app block has an identifier and the following settings: | Setting | Description | | ------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `name` | Name of the block used in the Page Builder interface. Also accepts a [`help` key](https://doc.ibexa.co/en/saas/content_management/pages/page_blocks/#block-name-and-help-text) that adds a helper text under the **Name** field in the block configuration form. | | `category` | Category in the Page Builder **Page blocks** toolbox that the block is shown in. | | `thumbnail` | Thumbnail used in the Page Builder **Page blocks** toolbox. | | `component` | React App Component name used in `assets/page-builder/react/blocks` directory. | | `visible` | (Optional) Toggles the block's visibility in the Page Builder **Page blocks** toolbox. Remove the block from the layout before you publish another version of the page. | | `attributes` | (Optional) List of [block attributes](https://doc.ibexa.co/en/saas/content_management/pages/page_block_attributes/index.md). | For example: ```yaml ibexa_fieldtype_page: react_blocks: calculator: name: Calculator category: Demo thumbnail: /bundles/ibexaadminuiassets/vendors/ids-assets/dist/img/all-icons.svg#calendar component: Calculator attributes: a: type: integer b: integer ``` Each entry below `react_blocks` adds one block to the Page Builder with the defined name, category and thumbnail. Both name and attributes support a short syntax and a long one for specifics. `Attributes` defined without sub-keys use the key as the identifier and name, and the value as the type: ```yaml attributes: b: integer ``` Sub-keys can be used to specify any of the usual [attributes configuration](https://doc.ibexa.co/en/saas/content_management/pages/page_block_attributes/index.md) key: ```yaml attributes: a: name: Attribute A type: string options: ... ``` Apps that are registered this way must be configured and referenced in the semantic configuration to be registered as blocks. Parameters passed as props must be converted so that they can be used as the configured type in the app. ## Create React App block In the following example, you learn how to create the `Calculator` React App block [configured in the previous section's example](#react-app-block-configuration). ### Configure React App Block First, install React. Run `yarn add react` command. Next, create a .jsx file which describes your component. You can place it in any location. In the following example, create `Calculator.jsx` file in `assets/page-builder/components/` directory: ```js import React from 'react'; export default function (props) { // a + b = ... console.log("Hello React!"); return
    {props.a} + {props.b} = {parseInt(props.a) + parseInt(props.b)}!
    ; } ``` Then, create a `Calculator.js` file in `assets/page-builder/react/blocks` directory. Files in this directory create a map of Components which then are imported to `react.blocks.js` file. As a result, the components are rendered on the page. ```js import Calculator from '/assets/page-builder/components/Calculator'; export default { Calculator: Calculator, }; ``` Now, you should see new `Calculator` block in the Page Builder blocks list: ![Calculator](https://doc.ibexa.co/en/saas/content_management/img/calculator.png "Calculator - React App Block") Then, make sure that your [Page layout template](https://doc.ibexa.co/en/saas/templating/templates/template_configuration/#page-layout) (like `templates/themes/standard/pagelayout.html.twig`) has the following Twig code in its `{% block javascripts %}`: ```twig {% if encore_entry_exists('react-blocks-js') %} {{ encore_entry_script_tags('react-blocks-js') }} {% endif %} ``` # Ibexa Connect scenario block > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). Work with Ibexa Connect scenario block that retrieves and displays data from an Ibexa Connect webhook. Editions: Experience Ibexa Connect scenario block retrieves and displays data from an Ibexa Connect webhook. Scenario block is a regular [Page block](https://doc.ibexa.co/en/saas/content_management/pages/page_blocks/index.md) and can be configured on field definition level as any other block. > **Caution: Caution** > > When setting up your instance, ensure you have profiler enabled. To set up Page Builder in Cohesivo, follow the [Page and Form tutorial](https://doc.ibexa.co/en/saas/tutorials/page_and_form_tutorial/page_and_form_tutorial/index.md). ## Scenario block configuration In the following example you can learn how to configure Ibexa Connect scenario block with two available templates: `company_customers` and `external_clients`. ### Block templates First, in `config/packages/ibexa_connect.yaml` add the following configuration: ```yaml ibexa_connect: scenario_block: block_templates: company_customers: template: 'blocks/default.html.twig' external_clients: label: External clients template: 'blocks/default.html.twig' parameters: external_client_id: string external_client_name: type: string required: true ``` For each block template you can set up additional settings, for example, label, type or parameters. ### Define page layouts To preview your block in the frontend, define page layouts in `config/packages/views.yaml` directory. This file defines, which layouts are used to render Page Builder. ```yaml ibexa: system: site: page_layout: pagelayout.html.twig user: layout: pagelayout.html.twig ``` You also need to create `pagelayout.html.twig` file in `templates` folder: ```html+twig {% if content is defined %} {% set title = ez_content_name(content) %} {% endif %} {{ title|default('Home'|trans) }} - {{ "It's a Dog's World!"|trans }}
    {% block content %}{% endblock %}
    ``` Then, in `templates/blocks` directory under `default.html.twig`, provide your block configuration: ```html+twig {{ dump(ibexa_connect_data) }} ``` In the following example, the configuration of the block is non-complex - block is only used to display the content transferred from an Ibexa Connect webhook. At this point the Ibexa Connect scenario block is ready to be used in Page Builder. ### Configure Ibexa Connect scenario block in Page Builder Now, you can configure Ibexa Connect scenario block in Page Builder. To do it, in your Page add Ibexa Connect block by dragging it from the menu to a drop zone and enter block settings. - In the **Basic** tab in **Webhook link** field, provide a link to an Ibexa Connect webhook, for example, `https://connect.ibexa.co/3/scenarios/688/edit`: ![Ibexa Connect Basic tab](https://doc.ibexa.co/en/saas/content_management/img/ibexa_connect_basic_tab.png) - In the **Design** tab, choose one of declared templates, in the following example, `company_customers` or `External clients`. To do it, extend drop-down list in the **View** field and choose one of the available options. ![Ibexa Connect Design tab](https://doc.ibexa.co/en/saas/content_management/img/ibexa_connect_design_tab.png) Click **Submit** button to confirm. After submitting the block, page refreshes and Ibexa Connect block displays data from provided Ibexa Connect webhook. ![Ibexa Connect webhook preview](https://doc.ibexa.co/en/saas/content_management/img/ibexa_connect_webhook_preview.png) # Forms > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). Forms are a type of content item that you can use to improve the functionality of your website. Editions: Experience Forms are a type of content item that you can use to improve the functionality of your website. - [Form Builder product guide](https://ez-systems-developer-documentation--3381.com.readthedocs.build/en/3381/content_management/forms/form_builder_guide/): See the Form Builder product guide and learn how to create various forms to increase the functionality of your website. - [Forms](https://ez-systems-developer-documentation--3381.com.readthedocs.build/en/3381/content_management/forms/work_with_forms/): Form Builder enables creating dynamic forms to use in surveys, questionnaires, sign-up forms and others. - [Form API](https://ez-systems-developer-documentation--3381.com.readthedocs.build/en/3381/content_management/forms/form_api/): You can use PHP API to get, create and delete form submissions. - [Create Form Builder Form attribute](https://ez-systems-developer-documentation--3381.com.readthedocs.build/en/3381/content_management/forms/create_form_attribute/): Create Form Builder Form attribute. - [Create custom Form field](https://ez-systems-developer-documentation--3381.com.readthedocs.build/en/3381/content_management/forms/create_custom_form_field/): Extend a Form with a custom Form field to fit your particular needs. - [Customize email notifications](https://ez-systems-developer-documentation--3381.com.readthedocs.build/en/3381/content_management/forms/customize_email_notifications/): Adapt the form and content of emails sent out from the Form Builder. # Form Builder product guide > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). See the Form Builder product guide and learn how to create various forms to increase the functionality of your website. Editions: Experience ## What is Form Builder Form Builder is a tool that lets you build forms consisting of different fields. By adding forms on the website, you can increase its functionality and improve user experience. Use Form Builder to create various forms, such as survey, questionnaire, sign-up form, using basic form fields available in the Form Builder. You can also manage your forms and review the results gathered from the website users. ## Availability Form Builder is available in Ibexa Experience and Ibexa Commerce. ## How does Form Builder work ### Form Builder interface Form Builder user interface consists of: A. Drop zone B. Form fields toolbar C. Save button D. Search bar E. Discard button ![Form Builder interface](https://doc.ibexa.co/en/saas/content_management/forms/img/form_builder_interface.png) ### Form fields To create forms, you can use available form fields or create custom ones. The available basic form fields are: | Field name | Icon | Description | | ------------------- | ------------------- | -------------------------------------------------------------------------- | | Single line input | Single line input | Single line field for short text. | | Multiple line input | Multiple line input | Multiple line field for longer text. | | Number | Number | Field to set up a number using arrows. | | Checkbox | Checkbox | Single checkbox element with one option value available. | | Checkboxes | Checkboxes | Multiple checkboxes with more than one option values available. | | Radio | Radio | List with multiple option values available and visible. | | Dropdown | Dropdown | Dropdown list with multiple option values available. | | Email | Email | Field to insert an email address. | | Date | Date | Field to insert a date. | | URL | URL | Field to insert an URL address. | | File | File | Interactive field to upload file. | | Captcha | Captcha | Field with captcha and additional blank line to rewrite it. | | Button | Button | Form submit button. | | Hidden field | Hidden field | Field used to submit metadata that should not be visible in rendered form. | ### Create a form Editors can use the created form anywhere on the website. Forms can be used in page blocks, embedded in the online editor or even used as a field relation. The same form can be placed at multiple locations on the website. To learn more, see [Work with forms](https://doc.ibexa.co/projects/userguide/en/6.0/content_management/work_with_forms/). ### Forms management [Form](https://doc.ibexa.co/en/saas/content_management/forms/work_with_forms/index.md) is one of available [content items](https://doc.ibexa.co/projects/userguide/en/6.0/content_management/content_items/) that you can find in the platform. You can work with it as with other regular items, for example, create new one, edit existing one, or move. You can manage all the existing forms. To do it, in a selected place of the content tree find your form and click on it. In this window you can see all the information about your form, view submissions, create versions, and more Using the buttons in the right corner, you can also edit, move, copy, hide, or send your form to the trash. ![Forms management](https://doc.ibexa.co/en/saas/content_management/forms/img/forms_management.png) ### Form API To manage form submissions created in the Form Builder, use `FormSubmissionServiceInterface`. You can get existing form submission and create or delete one. Detailed instruction of getting, creating and deleting form submissions, you can find in Ibexa Developer Documentation in [Form API page](https://doc.ibexa.co/en/saas/content_management/forms/form_api/index.md). ### Extend Form Builder You can extend the Form Builder by adding new Form fields or modifying existing ones. To create new form fields, you need to [define them in configuration](https://doc.ibexa.co/en/saas/content_management/forms/create_custom_form_field/index.md). Fields or fields attributes [can be modified](https://doc.ibexa.co/en/saas/content_management/forms/create_custom_form_field/#modify-existing-form-fields) by subscribing `ibexa.form_builder.field.` or `ibexa.form_builder.field..` events. ### Create new Form attribute Each Form has available attributes, for example, string, text, or location. You can also [create a Form attribute](https://doc.ibexa.co/en/saas/content_management/forms/create_form_attribute/index.md) for new Form fields or existing ones. To do it, you have to: 1. define a new Form attribute in the configuration, 2. create a mapper, 3. add Symfony form type, 4. customize Form templates, 5. add scripts, 6. implement field, 7. implement field mapper, 8. create submission converter. ### View results You can preview the results of each published form. To do it, go to **Submissions** tab in the content item view: ![View results](https://doc.ibexa.co/en/saas/content_management/forms/img/view_results.png) Here you can view the details of each submission or delete any of them. The **Download submissions** button enables you to download all the submissions in a .CSV (comma-separated value) file. > **Tip: Restricting access to form submissions** > > By default, back office users with access to the form content item can access the form submissions. > > If your form submissions require stricter access control than the form itself, you can introduce a [dedicated policy that manages access to submission data](https://doc.ibexa.co/en/saas/permissions/custom_policies/#restrict-access-to-form-submissions). ## Benefits ### General overview With Form Builder you're allowed to build an unlimited number of forms. These forms can be used anywhere on the website and are ready to start collecting information. Form Builder interface is plain, which makes the creation of forms fast and intuitive. ### Forms management Forms can be managed simply and effectively: you can copy them, move, organize into folders, create versions, and delete if necessary. Each field can be configured so that the form collects the exact details that you need. ### Custom Form fields With Form Builder you can use existing Form fields, but also you can extend it by adding new or modifying existing ones. This allows you to create forms that fit your needs. ### Analytic tool All the submissions can are visible in **Submissions** tab. You can download them as a .CSV file for additional analysis. # Forms > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). Form Builder enables creating dynamic forms to use in surveys, questionnaires, sign-up forms and others. Editions: Experience You can build forms consisting of different fields in the Form Builder. > **Tip: Tip** > > To learn how to get, create, and delete form submissions by using the PHP API, see [Form API](https://doc.ibexa.co/en/saas/content_management/forms/form_api/index.md). > **Caution: Known limitation** > > To have multiple instances of the same form on one page, create several identical form blocks. Otherwise, you may encounter issues with submitting data from all forms at the same time. ## Existing Form fields ### Captcha field The Captcha Form field is based on [Gregwar/CaptchaBundle](https://github.com/Gregwar/CaptchaBundle). ![Captcha field](https://doc.ibexa.co/en/saas/content_management/img/extending_form_builder_captcha_default.png) You can customize the field by adding configuration to `config/packages/gregwar_captcha.yaml` under `gregwar_captcha`: ```yaml gregwar_captcha: as_url: true width: 150 invalid_message: Code does not match, please retry. reload: true ``` The example configuration above resizes the Captcha image (line 3), changes the error message (line 4), and enables the user to reload the code (line 5). ![Custom captcha field](https://doc.ibexa.co/en/saas/content_management/img/extending_form_builder_captcha_result.png) For information about available options, see [Gregwar/CaptchaBundle's documentation](https://github.com/Gregwar/CaptchaBundle#options). > **Note: Note** > > If your installation uses Varnish to manage content cache, you must modify the configuration to avoid issues with the Captcha field. For more information, see [Ensure proper captcha behavior](https://doc.ibexa.co/en/saas/infrastructure_and_maintenance/cache/http_cache/reverse_proxy/#ensure-proper-captcha-behavior). ## Form submission purging You can purge all submissions of a given form. To do this, run the following command, where `form-id` stands for Content ID of the form for which you want to purge data: ```bash php bin/console ibexa:form-builder:purge-form-submissions [options] [--] ``` The following table lists some of the available options and their meaning: | Switch | Option | Description | | ------ | ------------------------------- | ------------------------------------------------------------------------------------------------------------------------- | | `-l` | `--language-code=LANGUAGE-CODE` | Passes a language code, for example, "eng-GB". | | `-u` | `--user[=USER]` | Passes a repository username. By default it's "admin". | | `-c` | `--batch-size[=BATCH-SIZE]` | Passes a number of URLs to check in a single iteration. Set it to avoid using too much memory. By default it's set to 50. | | | `--siteaccess[=SITEACCESS]` | Passes a SiteAccess to use for operations. If not provided, the default SiteAccess is used. | ## Form-uploaded files You can use Forms to enable the user to upload files. The default location for files uploaded in this way is `/Media/Files/Form Uploads`. You can change it with the following configuration: ```yaml ibexa: system: default: form_builder: upload_location_id: 54 ``` This applies only if no specific location is defined in the Form itself. # Form API > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). You can use PHP API to get, create and delete form submissions. Editions: Experience ## Form submissions To manage form submissions created in the [Form Builder](https://doc.ibexa.co/en/saas/content_management/forms/form_builder_guide/index.md), use `FormSubmissionServiceInterface`. > **Tip: Restricting access to form submissions** > > By default, back office users with access to the form content item can access the form submissions. > > If your form submissions require stricter access control than the form itself, you can introduce a [dedicated policy that manages access to submission data](https://doc.ibexa.co/en/saas/permissions/custom_policies/#restrict-access-to-form-submissions). ### Getting form submissions To get existing form submissions, use `FormSubmissionServiceInterface::loadByContent()` (which takes a `ContentInfo` object as parameter), or `FormSubmissionServiceInterface::loadById()`. ```php $submissions = $this->formSubmissionService->loadByContent($contentInfo); ``` Through this object, you can get information about submissions, such as their total number, and submission contents. ```php $output->writeln('Total number of submissions: ' . $submissions->getTotalCount()); foreach ($submissions as $sub) { $output->write($sub->getId() . '. submitted on '); $output->write($sub->getCreated()->format('Y-m-d H:i:s') . ' by '); $output->writeln((string) $this->userService->loadUser($sub->getUserId())->getName()); foreach ($sub->getValues() as $value) { $output->writeln('- ' . $value->getIdentifier() . ': ' . $value->getDisplayValue()); } } ``` ### Creating form submissions To create a form submission, use the `FormSubmissionServiceInterface::create()` method. This method takes: - the `ContentInfo` object of the content item containing the form - the language code - the value of the field containing the form - the array of form field values ```php $formValue = $content->getFieldValue('form', 'eng-GB')->getFormValue(); $data = [ ['id' => 7, 'identifier' => 'single_line', 'name' => 'Line', 'value' => 'The name'], ['id' => 8, 'identifier' => 'number', 'name' => 'Number', 'value' => 123], ['id' => 9, 'identifier' => 'checkbox', 'name' => 'Checkbox', 'value' => 0], ]; $this->formSubmissionService->create( $contentInfo, 'eng-GB', $formValue, $data ); ``` ### Deleting form submissions You can delete a form submission by using the `FormSubmissionServiceInterface::delete()` method. ```php $submission = $this->formSubmissionService->loadById(29); $this->formSubmissionService->delete($submission); ``` # Create custom Form field > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). Extend a Form with a custom Form field to fit your particular needs. Editions: Experience You can extend the Form Builder by adding new Form fields or modifying existing ones. Define new form fields in configuration. ## Configure Form field For example, to create a Country Form field in the "Custom form fields" category, provide the following configuration under the `ibexa_form_builder.fields` [configuration key](https://doc.ibexa.co/en/saas/administration/configuration/configuration/#configuration-files): ```yaml ibexa_form_builder: fields: country: name: country_field.name category: custom_category.name thumbnail: '/bundles/ibexaadminuiassets/vendors/ids-assets/dist/img/all-icons.svg#pins-locations' attributes: label: name: country_field.label.name type: string validators: not_blank: message: You must provide a label for the field help: name: country_field.help.name type: string validators: required: ~ ``` and provide the translations for the labels in `translations/ibexa_form_builder.en.yaml`: ```yaml country_field.name: Country custom_category.name: Custom form fields country_field.label.name: Display label country_field.help.name: Help text ``` Available attribute types are: | Type | Description | | ---------- | ------------------------- | | `string` | String | | `text` | Text block | | `integer` | Integer number | | `url` | URL | | `multiple` | Multiple choice | | `select` | Dropdown | | `checkbox` | Checkbox | | `location` | Content location | | `radio` | Radio button | | `action` | Button | | `choices` | List of available options | Each type of Form field can have validators of the following types: - `required` - `min_length` - `max_length` - `min_choices` - `max_choices` - `min_value` - `max_value` - `regex` - `upload_size` - `extensions` ## Create mapper New types of fields require a mapper which implements the `Ibexa\Contracts\FormBuilder\FieldType\Field\FieldMapperInterface` interface. To create a Country field type, implement the `FieldMapperInterface` interface in `src/FormBuilder/Field/Mapper/CountryFieldMapper.php`: ```php getAttributeValue('label'); $options['help'] = $field->getAttributeValue('help'); return $options; } } ``` Then, register the mapper as a service: ```yaml services: App\FormBuilder\Field\Mapper\CountryFieldMapper: arguments: $fieldIdentifier: country $formType: Symfony\Component\Form\Extension\Core\Type\CountryType tags: - { name: ibexa.form_builder.field.mapper } ``` Now you can go to back office and build a new form. You should be able to see the new section in the list of available fields: ![Custom form fields](https://doc.ibexa.co/en/saas/content_management/img/extending_form_builder_custom_form_fields.png) And a new Country Form field: ![Country field](https://doc.ibexa.co/en/saas/content_management/img/extending_form_builder_country_field.png) ## Modify existing Form fields Field or field attribute definition can be modified by subscribing to one of the following events: - `ibexa.form_builder.field.` - `ibexa.form_builder.field..` The following example adds a `custom` string attribute to `single_line` field definition. ```php 'onSingleLineFieldDefinition', ]; } public function onSingleLineFieldDefinition(FieldDefinitionEvent $event): void { $isReadOnlyAttribute = new FieldAttributeDefinitionBuilder(); $isReadOnlyAttribute->setIdentifier('custom'); $isReadOnlyAttribute->setName('Custom attribute'); $isReadOnlyAttribute->setType('string'); $definitionBuilder = $event->getDefinitionBuilder(); $definitionBuilder->addAttribute($isReadOnlyAttribute->buildDefinition()); } } ``` Register this subscriber as a service: ```yaml services: App\EventSubscriber\FormFieldDefinitionSubscriber: public: true tags: - kernel.event_subscriber ``` ## Access Form field definitions Field definitions are accessible through: - `Ibexa\FormBuilder\Definition\FieldDefinitionFactory` in the back end - global variable `ibexa.formBuilder.config.fieldsConfig` in the front end # Create Form Builder Form attribute > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). Create Form Builder Form attribute. Editions: Experience You can create a Form attribute for new Form fields or existing ones. To do it, you have to define a new Form attribute in the configuration. In the following example you can learn how to create the new Form with `richtext_description` attribute that allows you to add formatted description to the Form. ## Configure Form attribute To create a `richtext_description` attribute, add the following configuration under the `ibexa_form_builder.fields` [configuration key](https://doc.ibexa.co/en/saas/administration/configuration/configuration/#configuration-files): ```yaml ibexa_form_builder: fields: checkbox_with_richtext_description: name: Checkbox with Rich Text description category: Default thumbnail: '/bundles/ibexaadminuiassets/vendors/ids-assets/dist/img/all-icons.svg#form-input-single-line' attributes: label: name: Label type: string validators: not_blank: message: You must provide label of the field richtext_description: name: 'Description' type: 'richtext_description' validators: required: ~ ``` ## Create mapper The new Form attribute requires a `FieldAttributeTypeMapper`. Register the mapper as a service in `config/services.yaml`: ```yaml App\FormBuilder\FieldType\Field\Mapper\CheckboxWithRichtextDescriptionFieldMapper: arguments: $fieldIdentifier: checkbox_with_richtext_description $formType: 'App\FormBuilder\Form\Type\CheckboxWithRichtextDescriptionType' tags: - { name: ibexa.form_builder.field.mapper } ibexa.form_builder.attribute_form_type_mapper.richtext_description: class: Ibexa\FormBuilder\Form\Mapper\FieldAttribute\GenericFieldAttributeTypeMapper arguments: $formTypeClass: App\FormBuilder\Form\Type\FieldAttribute\AttributeRichtextDescriptionType $typeIdentifier: 'richtext_description' tags: - { name: ibexa.form_builder.form.type.attribute.mapper } App\FormBuilder\FormSubmission\Converter\RichtextDescriptionFieldSubmissionConverter: arguments: $typeIdentifier: 'checkbox_with_richtext_description' $twig: '@twig' tags: - { name: ibexa.form_builder.field.submission.converter } ``` ## Add Symfony form type The attribute must be editable for the form creator, so it needs to have a Symfony form type. Add an `AttributeRichtextDescriptionType.php` file with the form type in the `src/FormBuilder/Form/Type/FieldAttribute` directory: ```php {% set udw_context = { 'languageCode': 'en', } %} {{ form_errors(form) }} {{ form_row(form) }} {{ encore_entry_script_tags('formbuilder-richtext-checkbox-js') }} {% endblock %} ``` - `templates/themes//formtheme/formbuilder_checkbox_with_richtext_description.html.twig`: ```html+twig {% block checkbox_with_richtext_description_row %} {{ form_label(form)}} {{ form_errors(form) }} {{ form_widget(form) }} {{ form.vars.richtextDescription|ibexa_richtext_to_html5() }} {% endblock %} ``` Then, specify the new template in configuration, under the `twig.form_themes` configuration key: ```yaml twig: form_themes: - '@ibexadesign/formtheme/formbuilder_checkbox_with_richtext_description.html.twig' ``` ## Add scripts Now you need to enable the Rich Text editor. Provide the required script in a new `assets/js/formbuilder-richtext-checkbox.js` file: ```js (function (global, doc, ibexa) { global.addEventListener('load', (event) => { const richtext = new ibexa.BaseRichText(); // Enable editor in all ibexa-data-source divs doc.querySelectorAll('.ibexa-data-source').forEach((ibexaDataSource) => { const richtextContainer = ibexaDataSource.querySelector('.ibexa-data-source__richtext'); if (richtextContainer.classList.contains('ck')) { return; } richtext.init(richtextContainer); }); }); const openUdw = (config) => { const openUdwEvent = new CustomEvent('ibexa-open-udw', { detail: config }); doc.body.dispatchEvent(openUdwEvent); }; ibexa.addConfig('richText.alloyEditor.callbacks.selectContent', openUdw); })(window, window.document, window.ibexa); ``` Then, paste the highlighted part of the code into the `webpack.config.js` file: ```js const Encore = require('@symfony/webpack-encore'); const path = require('path'); const getIbexaConfig = require('./ibexa.webpack.config.js'); const ibexaConfig = getIbexaConfig(Encore); const customConfigs = require('./ibexa.webpack.custom.configs.js'); const { isReactBlockPathCreated } = require('./ibexa.webpack.config.react.blocks.js'); Encore.reset(); Encore .setOutputPath('public/build/') .setPublicPath('/build') .enableStimulusBridge('./assets/controllers.json') .enableSassLoader() .enableReactPreset() .enableSingleRuntimeChunk() .copyFiles({ from: './assets/images', to: 'images/[path][name].[ext]', pattern: /\.(png|svg)$/ }) .configureBabel((config) => { config.plugins.push('@babel/plugin-proposal-class-properties'); }) // enables @babel/preset-env polyfills .configureBabelPresetEnv((config) => { config.useBuiltIns = 'usage'; config.corejs = 3; }) ; // Welcome page stylesheets Encore.addEntry('welcome-page-css', [ path.resolve(__dirname, './assets/scss/welcome-page.scss'), ]); // Welcome page javascripts Encore.addEntry('welcome-page-js', [ path.resolve(__dirname, './assets/js/welcome.page.js'), ]); if (isReactBlockPathCreated) { // React Blocks javascript Encore.addEntry('react-blocks-js', './assets/js/react.blocks.js'); } Encore.addEntry('app', './assets/app.js'); Encore.addEntry('formbuilder-richtext-checkbox-js', './assets/js/formbuilder-richtext-checkbox.js'); const projectConfig = Encore.getWebpackConfig(); projectConfig.name = 'app'; module.exports = [ibexaConfig, ...customConfigs, projectConfig]; // uncomment this line if you've commented-out the above lines // module.exports = [ eZConfig, ibexaConfig, ...customConfigs ]; ``` Clear the cache and regenerate the assets by running the following commands: ```bash php bin/console cache:clear php bin/console assets:install yarn encore dev ``` ## Implement field Now you have to implement the field, and make sure the value from the Rich Text attribute is passed on to the field form. Create a `src/FormBuilder/Form/Type/CheckboxWithRichtextDescriptionType.php` file. ```php setDefaults([ 'richtext_description' => '', ]); $resolver->setAllowedTypes('richtext_description', ['null', 'string']); } public function buildView(FormView $view, FormInterface $form, array $options): void { // pass the Dom object of the richtext doc to the template $dom = new \DOMDocument(); if (!empty($options['richtext_description'])) { $dom->loadXML($options['richtext_description']); } $view->vars['richtextDescription'] = $dom; } } ``` ## Implement field mapper To implement a field mapper, create a `src/FormBuilder/FieldType/Field/Mapper/CheckboxWithRichtextDescriptionFieldMapper.php` file. ```php getAttributeValue('label'); $options['richtext_description'] = $field->getAttributeValue('richtext_description'); return $options; } } ``` Now, the attribute value can be stored in the new Form. ## Create submission converter The new field is based on a checkbox, so to display the submissions of this field, you can use the `BooleanFieldSubmissionConverter`. Create a `src/FormBuilder/FormSubmission/Converter/RichtextDescriptionFieldSubmissionConverter.php` file. ```php **Forms** -> **Create content**, and select **Form**. You should be able to see the new section in the list of available fields: ![New form field](https://doc.ibexa.co/en/saas/content_management/forms/img/checkbox_with_richtext_description-item.png) When editing settings, the "Description" attribute has the Rich Text input. ![Field settings](https://doc.ibexa.co/en/saas/content_management/forms/img/checkbox_with_richtext_description-edit.png) When you enter the "Description" attribute, the Rich Text toolbar appears. ![Rich Text toolbar](https://doc.ibexa.co/en/saas/content_management/forms/img/checkbox_with_richtext_description-focus.png) The preview displays the formatted text along with the checkbox and its label. ![Field preview](https://doc.ibexa.co/en/saas/content_management/forms/img/checkbox_with_richtext_description-preview.png) # Customize email notifications > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). Adapt the form and content of emails sent out from the Form Builder. Editions: Experience Email is one of the **Submit** button options you can add to a form in the Form Builder. Use it to configure a list of email addresses that get notifications about newly filled forms. ![Email notification](https://doc.ibexa.co/en/saas/content_management/img/email_notification.png) ## Override email template To customize the form submission email, override the `form_builder/form_submit_notification_email.html.twig` template. It contains two blocks: `subject` and `body`. Each of them is rendered independently and consists of three sets of parameters. | Parameter | Type | Description | | --------- | ------------------------------------------------------------ | ---------------------------------- | | `content` | `Ibexa\Contracts\Core\Repository\Values\Content\Content` | Name of the form, its content type | | `form` | `Ibexa\Contracts\FormBuilder\FieldType\Model\Form` | Definition of the form | | `data` | `Ibexa\Contracts\FormBuilder\FieldType\Model\FormSubmission` | Sent data | ## Configure sender details Some email providers require a sender address to be set, so to avoid unsent emails when using Form Builder, it's recommended to configure `sender_address` in `config/packages/swiftmailer.yaml`. This email acts as a sender and return address for all bounced messages. > **Note: Note** > > Since November 2021 the Swift Mailer is no longer supported and the integration with Symfony is deprecated in Symfony 6.0. The Swift Mailer got replaced by the Symfony Mailer. Add `sender_address` entry to `config/packages/swiftmailer.yaml`: ```yaml swiftmailer: url: '%env(MAILER_URL)%' spool: { type: 'memory' } sender_address: '%env(MAILER_SENDER_ADDRESS)%' ``` In the `.env` file, define a new environment variable: `MAILER_SENDER_ADDRESS=mail@example.com` and configure your mail server connection details in the `MAILER_URL` environmental variable. # Workflow > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). Workflow controls how content items pass between stages and allows setting up editorial flows, for example for reviews and proofreading. The workflow functionality passes a content item version through a series of stages. For example, an editorial workflow can pass a content item from draft stage through design and proofreading. By default, Cohesivo comes pre-configured with a Quick Review workflow. You can disable the default workflow and define different workflows in configuration. Workflows are permission-aware. ## Workflow configuration Each workflow consists of stages and transitions between them. The following example configuration defines a workflow where you can optionally pass a draft to be checked by the legal team. ![Diagram of custom workflow](https://doc.ibexa.co/en/saas/content_management/img/workflow_custom_diagram.png) ```yaml ibexa: system: default: workflows: custom_workflow: name: Custom Workflow matchers: content_type: [article, folder] content_status: [draft] stages: draft: label: Draft color: '#f15a10' legal: label: Legal color: '#5a10f1' actions: notify_reviewer: ~ done: label: Done color: '#301203' last_stage: true initial_stage: draft transitions: to_legal: from: [draft] to: [legal] label: To legal color: '#8888ba' icon: '/bundles/ibexaadminuiassets/vendors/ids-assets/dist/img/all-icons.svg#alert-error' reviewers: required: true user_group: 13 back_to_draft: reverse: to_legal label: Back to draft color: '#cb8888' icon: '/bundles/ibexaadminuiassets/vendors/ids-assets/dist/img/all-icons.svg#arrow-left' approved_by_legal: from: [legal] to: [done] label: Approved by legal color: '#88ad88' icon: '/bundles/ibexaadminuiassets/vendors/ids-assets/dist/img/all-icons.svg#form-checkbox' actions: publish: ~ done: from: [draft] to: [done] label: Done color: '#88ad88' icon: '/bundles/ibexaadminuiassets/vendors/ids-assets/dist/img/all-icons.svg#form-checkbox' actions: publish: ~ ``` ### Matchers Matchers define when the workflow is used. Their configuration is optional. `content_type` contains an array of content type identifiers that use this workflow. `content_status` lists the statuses of content items which fall under this workflow. The available values are: `draft` and `published`. If set to `draft`, applies for new content (newly created). If set to `published`, applies for content that has already been published (for example, edit after the content was published). ```yaml matchers: content_type: [article, folder] content_status: [draft] ``` ### Stages Each stage in the workflow has an identifier and can have a label and a color. The optional `last_stage` key indicates that content in this stage doesn't appear on the dashboard or in Review Queue. One stage, listed under `initial_stage`, is the one that the workflow starts with. ```yaml stages: draft: label: Draft color: '#f15a10' legal: label: Legal color: '#5a10f1' actions: notify_reviewer: ~ done: label: Done color: '#301203' last_stage: true initial_stage: draft ``` ### Transitions Each transition has an identifier and can have a label, a color, and an icon. A transition must state between which stages it transitions (lines 3-4), or be `reverse` to a different transition (line 9). ```yaml transitions: to_legal: from: [draft] to: [legal] label: To legal color: '#8888ba' icon: '/bundles/ibexaadminuiassets/vendors/ids-assets/dist/img/all-icons.svg#alert-error' back_to_draft: reverse: to_legal label: Back to draft color: '#cb8888' icon: '/bundles/ibexaadminuiassets/vendors/ids-assets/dist/img/all-icons.svg#arrow-left' ``` ### Reviewers When moving a content item through a transition, the user can select a reviewer. Assigning a reviewer is mandatory if you set `reviewers.required` to `true` for this transition. You can restrict who can review the content item by setting `reviewers.user_group` to a location ID of the user group. To be able to search for users for review, the user must have the `content/read` policy without any limitation, or with a limitation that allows reading users. This means that, in addition to your own settings for this policy, you must add the /Users subtree to the limitation and add users in the [content type limitation](https://doc.ibexa.co/en/saas/permissions/limitation_reference/#content-type-limitation). ```yaml transitions: to_legal: from: [draft] to: [legal] label: To legal color: '#8888ba' icon: '/bundles/ibexaadminuiassets/vendors/ids-assets/dist/img/all-icons.svg#alert-error' reviewers: required: true ``` #### Notifications To ensure that the assigned reviewers get a notification of a transition, configure the `actions.notify_reviewer` action for a stage. ```yaml legal: label: Legal color: '#5a10f1' actions: notify_reviewer: ~ ``` The notification is displayed in the user menu: ![Notification about content to review](https://doc.ibexa.co/en/saas/content_management/img/workflow_notification.png) #### Draft locking You can configure draft assignment in a way that when a user sends a draft to review, only the first editor of the draft can either edit the draft or unlock it for editing, and no other user can take it over. Use the [Version Lock limitation](https://doc.ibexa.co/en/saas/permissions/limitation_reference/#version-lock-limitation), set to "Assigned only", together with the `content/edit` and `content/unlock` policies to prevent users from editing and unlocking drafts that are locked by another user. ### Content publishing You can automatically publish a content item once it goes through a specific transition. To do so, configure the `publish` action for the transition: ```yaml done: from: [draft] to: [done] label: Done color: '#88ad88' icon: '/bundles/ibexaadminuiassets/vendors/ids-assets/dist/img/all-icons.svg#form-checkbox' actions: publish: ~ ``` ### Disable Quick Review You can disable the default workflow, for example, if your project doesn't use workflows, or Quick Review entries clog your database: ```yaml ibexa: system: default: workflows: quick_review: name: Quick Review matchers: content_type: [] ``` ## Custom actions Besides the built-in actions of publishing content and notifying the reviewers, you can also [create custom workflow actions](https://doc.ibexa.co/en/saas/content_management/workflow/add_custom_workflow_action/index.md). ## Workflow event timeline Workflow event timeline displays workflow transitions. You can also use it to render custom entries in the timeline, for example system alerts on workflows. ### Custom entry type To add a custom entry type, create a custom class extending `Ibexa\Workflow\WorkflowTimeline\Value\AbstractEntry`. Use an `Ibexa\Contracts\Workflow\Event\TimelineEvents::COLLECT_ENTRIES` event to add your entries to the timeline. ### Custom templates To provide custom templates for new event timeline entries, use the following configuration: ```yaml ibexa: system: default: workflows_config: timeline_entry_templates: - { template: '@IbexaWorkflow/ibexa_workflow/timeline/entries.html.twig', priority: 10 } ``` The template has to provide a block named `ez_workflow_timeline_entry_{ENTRY_IDENTIFIER}`. ## Permissions You can limit access to workflows at stage and transition level. The `workflow/change_stage` policy grants permission to change stages in a specific workflow. You can limit this policy with the [Workflow Transition limitation](https://doc.ibexa.co/en/saas/permissions/limitation_reference/#workflow-transition-limitation) to only allow sending content in the selected transition. For example, by using the example above, a `workflow/change_stage` policy with `WorkflowTransitionLimitation` set to `Approved by legal` allows a legal team to send content forward after they're done with their review. You can also use the [Workflow Stage Limitation](https://doc.ibexa.co/en/saas/permissions/limitation_reference/#workflow-stage-limitation) together with the `content/edit` and `content/publish` Policies to limit the ability to edit content in specific stages. For example, you can use it to only allow a legal team to edit content in the `legal` stage. ## Validation ### Validate form before workflow transition By default, sending content to the next stage of the workflow doesn't validate the form in UI, so with the publish action, the form isn't verified for errors in UI. However, during the publish action, the sent form is validated in the service. Therefore, if there are any errors in the form, you return to the edit page but errors aren't triggered, which can be confusing when you have two or more tabs. To enable form validation in UI before sending it to the next stage of the workflow, add `validate: true` to the transitions of the stage. In the example below the form is validated in two stages: `to_legal` and `done`: ```yaml transitions: to_legal: from: [draft] to: [legal] label: To legal color: '#8888ba' icon: '/bundles/ibexaadminuiassets/vendors/ids-assets/dist/img/all-icons.svg#alert-error' reviewers: required: true user_group: 13 actions: legal_transition_action: data: message: "Sent to the legal department" validate: true back_to_draft: reverse: to_legal label: Back to draft color: '#cb8888' icon: '/bundles/ibexaadminuiassets/vendors/ids-assets/dist/img/all-icons.svg#arrow-left' from: [draft] to: [done] label: Done color: '#88ad88' icon: '/bundles/ibexaadminuiassets/vendors/ids-assets/dist/img/all-icons.svg#form-checkbox' actions: publish: ~ validate: true ``` You can check validation for a particular stage of the workflow even if the stage doesn't have any actions. # Workflow API > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). PHP API enables you to get workflow information and apply specific workflow transitions. You can manage [workflows](https://doc.ibexa.co/en/saas/content_management/workflow/workflow/index.md) with PHP API by using [`WorkflowServiceInterface`](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-Workflow-Service-WorkflowServiceInterface.html). ## Workflow service Workflow uses the Symfony [Workflow Component](https://symfony.com/doc/7.4/workflow.html), extended in the workflow service. The service implements the following methods: - `start` - places a content item in a workflow - `apply` - performs a transition - `can` - checks if a transition is possible The methods `apply` and `can` are the same as in Symfony Workflow, but the implementation in workflow service extends them, for example by providing messages. ## Getting workflow information To get information about a specific workflow for a content item, use [`WorkflowServiceInterface::loadWorkflowMetadataForContent`](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-Workflow-Service-WorkflowServiceInterface.html#method_loadWorkflowMetadataForContent): ```php $workflowMetadata = $this->workflowService->loadWorkflowMetadataForContent($content, $workflowName); foreach ($workflowMetadata->markings as $marking) { $output->writeln($content->getName() . ' is in stage ' . $marking->name . ' in workflow ' . $workflowMetadata->workflow->getName()); } ``` > **Tip: Tip** > > `marking`, a term from [Symfony Workflow](https://symfony.com/doc/7.4/workflow.html), refers to a state in a workflow. If you already have a [`VersionInfo`](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-Core-Repository-Values-Content-VersionInfo.html) object, use [`WorkflowServiceInterface::loadWorkflowMetadataForVersionInfo`](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-Workflow-Service-WorkflowServiceInterface.html#method_loadWorkflowMetadataForVersionInfo) to avoid loading the full [`Content`](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-Core-Repository-Values-Content-Content.html). This method is more efficient when iterating over draft versions: ```php $versionInfo = $content->getVersionInfo(); $workflowMetadataByVersion = $this->workflowService->loadWorkflowMetadataForVersionInfo($versionInfo, $workflowName); ``` To get a list of all workflows that can be used for a given content item, use [`WorkflowRegistryInterface::getSupportedWorkflows`](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-Workflow-Registry-WorkflowRegistryInterface.html#method_getSupportedWorkflows): ```php $supportedWorkflows = $this->workflowRegistry->getSupportedWorkflows($content); ``` ## Applying workflow transitions To place a content item in a workflow, use [`WorkflowServiceInterface::start`](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-Workflow-Service-WorkflowServiceInterface.html#method_start): ```php $this->workflowService->start($content, $workflowName); ``` To apply a transition to a content item, use `Workflow::apply`. Additionally, you can check if the transition is possible for the given object by using [`WorkflowServiceInterface::can`](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-Workflow-Service-WorkflowServiceInterface.html#method_can): ```php if ($this->workflowService->can($workflowMetadata, $transitionName)) { $workflow = $this->workflowRegistry->getWorkflow($workflowName); $workflow->apply($workflowMetadata->content, $transitionName, ['message' => 'done', 'reviewerId' => 14]); $output->writeln('Moved ' . $content->getName() . ' through transition ' . $transitionName); } ``` > **Tip: Tip** > > `Ibexa\Workflow\Value\WorkflowMetadata` object contains all information about a workflow, such as ID, name, transitions and current stage. `Ibexa\Workflow\Value\WorkflowMetadata::$workflow` gives you direct access to native Symfony Workflow object. # Add custom workflow action > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). Add custom actions that are performed during specific workflow transitions. Built-in workflow actions enable you to [automatically publish a content item](https://doc.ibexa.co/en/saas/content_management/workflow/workflow/#content-publishing) or to [send a notification to reviewers](https://doc.ibexa.co/en/saas/content_management/workflow/workflow/#notifications). You can also create custom actions that are called when content reaches a specific stage or goes through a transition in a workflow. The following example shows how to configure two custom actions that send customized notifications. ## Configure custom action Configure the first custom action under the `ibexa.system..workflows` [configuration key](https://doc.ibexa.co/en/saas/administration/configuration/configuration/#configuration-files): ```yaml ibexa: system: default: workflows: custom_workflow: transitions: to_legal: from: [draft] to: [legal] label: To legal color: '#8888ba' icon: '/bundles/ibexaadminuiassets/vendors/ids-assets/dist/img/all-icons.svg#alert-error' reviewers: required: true user_group: 13 actions: legal_transition_action: data: message: "Sent to the legal department" ``` The configuration indicates the name of the custom action (`legal_transition_action`). `data` contains additional data that is passed to the action. In this case, it's a message to display. ## Create event listener To define what the action does, create an event listener `src/EventListener/LegalTransitionListener.php`: ```php getActionMetadata($event->getWorkflow(), $event->getTransition()); $message = $metadata['data']['message'] ?? ''; $this->notificationHandler->info( $message, [], 'domain' ); $this->setResult($event, true); } } ``` This listener displays a notification bar at the bottom of the page when a content item goes through the `to_legal` transition. The content of the notification is the message configured in `actions.legal_transition_action.data`. To get it, access the metadata for this transition through `getActionMetadata()` (line 27). Register the listener as a service (in `config/services.yaml`): ```yaml services: App\EventListener\LegalTransitionListener: tags: - { name: ibexa.workflow.action.listener } ``` ## Use custom transition value Line 36 in the listener above sets a custom result value for the transition. You can use this value in other stages and transitions for this content item, for example: ```yaml approved_by_legal: from: [legal] to: [done] label: Approved by legal color: '#88ad88' icon: '/bundles/ibexaadminuiassets/vendors/ids-assets/dist/img/all-icons.svg#form-checkbox' actions: publish: ~ approved_transition_action: condition: - result.legal_transition_action == true ``` The action indicated here is performed only if the result from the `legal_transition_action` is set to `true`. Then, the following `src/EventListener/ApprovedTransitionListener` is called: ```php getContext(); $message = $context['message']; $this->notificationHandler->info( $message, [], 'domain' ); } } ``` Register this listener as a service: ```yaml services: App\EventListener\ApprovedTransitionListener: tags: - { name: ibexa.workflow.action.listener } ``` This listener also displays a notification, but in this case its content is taken from the message that the user types when choosing the `Done` transition. The message is contained in the context of the action. `$event->getContext()` (line 27) gives you access to the context. The context contains: - `$workflowId` - the ID of the current workflow - `$message` - content of the user's message when sending the content item through the transitions - `$reviewerId` - ID of the user who was selected as a reviewer - `$result` - an array of transition actions performed so far You can also modify the context using the `setContext()` method. For example, you can override the message typed by the user: ```php /** * @var array $context * @var \Symfony\Component\Workflow\Event\TransitionEvent $event */ $new_context = $context; $new_context['message'] = 'This article went through proofreading'; $event->setContext($new_context); ``` # URL management > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). Manage URL aliases and wildcards, and validate external URLs. You can manage external URL addresses and URL wildcards in the back office, **Admin** tab, the **URL Management** node. Configure URL aliases to have human-readable URL addresses throughout your system. ## Link manager When developing a site, users can enter links to external websites in either RichText or URL fields. Each such link is then displayed in the URL table. You can view and update all external links that exist within the site, without having to modify and re-publish the individual content items. The **Link manager** tab contains all the information about each link, including its status (valid or invalid) and the time the system last attempted to validate the URL address. Click an entry in the list to display its details and check which content items use this link. Edit the entry to update the URL address in all the occurrences throughout the website. > **Note: Note** > > When you edit the details of an entry to update the URL address, the status automatically changes to valid. ## External URL validation You can validate all the addresses from the URL table by executing the `ibexa:check-urls` command. It validates the links by accessing them one by one and updates the value in the Last checked field. If a broken link is found, its status is set to "invalid". The following protocols are currently supported: - `http` - `https` - `mailto` ### Enabling automatic URL validation To enable automatic URL validation, set up a scheduled task to run the `ibexa:check-urls` command periodically. ### Configuration The configuration of external URLs validation is SiteAccess-aware and is stored under the `ibexa.system..url_checker` [configuration key](https://doc.ibexa.co/en/saas/administration/configuration/configuration/#configuration-files), for example: ```yaml ibexa: system: default: url_checker: handlers: http: enabled: true batch_size: 64 https: enabled: true ignore_certificate: false mailto: enabled: false ``` Available options are protocol-specific. For details, see the tables below. #### http/https protocol | Option | Description | Default value | | ------------------ | -------------------------------------------------------------------------------------------- | ------------- | | enabled | Enables link validation. | true | | timeout | Defines the time that the request is allowed to take (in seconds). | 10 | | connection_timeout | Defines the time that the connect phase is allowed to take (in seconds). | 5 | | batch_size | Defines a maximum number of asynchronous requests. | 10 | | ignore_certificate | Decides if the peer's SSL certificate or the certificate name are verified against the host. | false | #### mailto protocol | Option | Description | Default value | | ------- | ------------------------ | ------------- | | enabled | Enables link validation. | true | For more information about Ibexa configuration, see [Configuration](https://doc.ibexa.co/en/saas/administration/configuration/configuration/index.md). ### Custom protocol support You can extend the external URL address validation with a custom protocol. To do this, you must provide a service that implements the [`Ibexa\Bundle\Core\URLChecker\URLHandlerInterface`](https://github.com/ibexa/core/blob/5.0/src/bundle/Core/URLChecker/URLHandlerInterface.php) interface. Then you must register the service with an `ibexa.url_checker.handler` tag, like in the following example: ```yaml app.url_checker.handler.custom: class: 'App\URLChecker\Handler\CustomHandler' tags: - { name: ibexa.url_checker.handler, scheme: custom } ``` The `scheme` attribute is mandatory and has to correspond to the name of the protocol, for instance, `ftp`. ## URL aliases You can define URL aliases for individual content items, for example, when you reorganize the content, and want to provide users with continuity. For each URL alias definition the history of changes is preserved, so that users who have bookmarked the URL addresses of content items can still find the information they desire. > **Note: Note** > > Make sure that you correctly define languages used by the site in the configuration (under the `ibexa.system..languages` key). Otherwise, redirections for the renamed Content with translations in multiple languages may fail to work properly. > **Caution: Legacy storage engine limitation** > > The [Legacy storage engine](https://doc.ibexa.co/en/saas/content_management/field_types/field_type_storage/#legacy-storage-engine) doesn't archive URL aliases, which initially had the same name in multiple languages. URL aliases aren't SiteAccess-aware. When creating an alias, you can select a SiteAccess to base it on. If the SiteAccess root path (configured in `content.tree_root.location_id`) is different than the default, the prefix path that results from the configured content root is prepended to the final alias path. ### URL alias pattern configuration You can configure how Cohesivo generates URL aliases. The configuration is stored under the `ibexa.url_alias.slug_converter` [configuration key](https://doc.ibexa.co/en/saas/administration/configuration/configuration/#configuration-files), for example: ```yaml ibexa: url_alias: slug_converter: transformation: example_group separator: dash transformation_groups: example_group: commands: - space_normalize - hyphen_normalize - apostrophe_normalize - doublequote_normalize - your_custom_command cleanup_method: url_cleanup ``` | Option | Description | | ----------------------- | --------------------------------------------------------------------------------------------------------- | | `transformation` | Indicates which pattern is used by default. | | `separator` | Decides what separator is used. There are three types of separator available: dash, underscore and space. | | `transformation_groups` | Contains the available patterns for URL generation. | A transformation group consists of an array of commands (see [all available commands](https://github.com/ibexa/core/tree/6.0/src/lib/Resources/slug_converter/transformations)) and a [`cleanupText`](https://github.com/ibexa/core/blob/6.0/src/lib/Persistence/Legacy/Content/UrlAlias/SlugConverter.php#L286). You can make use of pre-defined transformation groups. You can also add your own, with your own set of commands. To add commands to an existing group, provide the group name and list the commands that you want to add. ### Regenerating URL aliases You can use the `ibexa:urls:regenerate-aliases` command to regenerate all URL aliases. After the command is applied, old aliases redirect to the new ones. Use it when: - you change URL alias configuration and want to regenerate old aliases - you encounter database corruption - you have content that doesn't have a URL alias > **Caution: Caution** > > Before you apply the command, back up your database and make sure it's not modified while the command is running. Execute the following command to regenerate aliases: ```bash bin/console ibexa:urls:regenerate-aliases ``` You can also extend the command with the following parameters: - `--iteration-count` — Defines how many locations are processed at once to reduce memory usage - `--location-id` — Regenerates URL addresses for specific locations only, for example, `ibexa:urls:regenerate-aliases --location-id=1 --location-id=2` ## URL wildcards With wildcards, you can change the URL address for many content items at the same time, by replacing a portion of the destination's URL address. For example, you might want to shorten the path, or make the path meaningful. For each URL wildcard definition you set the wildcard pattern and its destination. Also, you can decide whether the user sees the content at the address that uses wildcards (Direct type), or is redirected to the original URL address of the destination (Forward type). For example, a URL wildcard called `pictures/*/*` can use `media/images/{1}/{2}` as destination. In this case, accessing `/pictures/home/photo/` loads `/media/images/home/photo/`. You can configure URL wildcards either in the back office, or with the public PHP API. Before you configure URL wildcards, you must enable the feature in configuration: ```yaml ibexa: url_wildcards: enabled: true ``` ### Configuring URL wildcards in the back office The **URL wildcards** tab contains all the information about each URL wildcard. You can delete or modify existing entries, or create new ones. > **Note: Note** > > To be able to modify wildcard support settings in the user interface, you must have the `content/urltranslator` policy. For more information about permissions, see [Permissions](https://doc.ibexa.co/en/saas/permissions/permissions/index.md). ### Configuring URL wildcards with the public PHP API You can create URL wildcards with the public PHP API by using the `URLWildcardService` service: ```php /** @var \Ibexa\Contracts\Core\Repository\Repository $repository */ $source = 'pictures/*/*'; $destination = 'media/images/{1}/{2}'; $redirect = true; $urlWildcardService = $repository->getURLWildcardService(); $repository->sudo(static function ($repository) use ($urlWildcardService, $source, $destination, $redirect): void { $urlWildcardService->create($source, $destination, $redirect); }); ``` If `$redirect` is set to `true`, the redirection changes the URL address. If it's `false`, the old URL address is be used, with the new content. # URL API > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). The PHP API URLService enables searching for external URLs used in tech text and URL fields. [`URLService`](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-Core-Repository-URLService.html) enables you to find, load and update external URLs used in RichText and URL fields. To view a list of all URLs, use [`URLService::findUrls`](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-Core-Repository-URLService.html#method_findUrls) `URLService::findUrls` takes as argument a [`URLQuery`](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-Core-Repository-Values-URL-URLQuery.html), in which you need to specify: - query filter, for example, Section - Sort Clauses for URL queries - offset for search hits, used for paging the results - query limit. If value is `0`, search query doesn't return any search hits ```php // ... use Ibexa\Contracts\Core\Repository\URLService; use Ibexa\Contracts\Core\Repository\Values\URL\Query\Criterion; use Ibexa\Contracts\Core\Repository\Values\URL\Query\SortClause; use Ibexa\Contracts\Core\Repository\Values\URL\URLQuery; // ... $query = new URLQuery(); $query->filter = new Criterion\LogicalAnd( [ new Criterion\SectionIdentifier(['standard']), new Criterion\Validity(true), ] ); $query->sortClauses = [ new SortClause\URL(SortClause::SORT_DESC), ]; $query->offset = 0; $query->limit = 25; $results = $this->urlService->findUrls($query); ``` ## URL search reference For the reference of Search Criteria and Sort Clauses you can use in URL search, see [URL Search Criteria](https://doc.ibexa.co/en/saas/search/url_search_reference/url_search_criteria/index.md) and [URL Sort Clauses](https://doc.ibexa.co/en/saas/search/url_search_reference/url_search_sort_clauses/index.md). # User-generated content > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). You can enable users to create new content in the repository by using forms available in the front end of the site. Cohesivo comes with content edition features via the Symfony stack. They're meant to allow the implementation of user-generated content from the front end, without entering the back office. ## Creating a new draft The `content/create/draft` route enables you to create a new draft for the selected content item. Pass the ID of the content item as an argument. For example, `content/create/draft/59` creates a new draft of the content item with ID 59. ## Creating a content item without using a draft The `/content/edit/nodraft` route shows a content item creation form for a given content type: | Argument | Type | Description | | ----------------------- | --------- | -------------------------------------------------------------------------- | | `contentTypeIdentifier` | `string` | The identifier of the content type to create. Example: `folder`, `article` | | `languageCode` | `string` | Language code the content item must be created in. Example: `eng-GB` | | `parentLocationId` | `integer` | ID of the location the content item must be created in. Example: `2` | This means that `/content/create/nodraft/folder/eng-GB/2` enables you to create a Folder in English as a child of location with ID 2. A limited subset of field types is supported: - `TextLine` - `TextBlock` - `Selection` - `Checkbox` - `User` - `Date` - `DateAndTime` - `Time` - `Integer` - `Float` - `URL` ## Editing a content item To edit an existing draft, use the `/content/edit/draft/` route, with the following arguments: | Argument | Type | Description | | -------------- | --------- | ------------------------------------------------------------------------ | | `contentId` | `integer` | ContentId of the item to edit. | | `versionNo` | `integer` | Number of the version to edit. The version must be an unpublished draft. | | `languageCode` | `string` | Language code of the version. Example: `eng-GB` | For example, `/content/edit/draft/1/5/eng-GB` enables you to edit draft 5 of content item 1 in English. ## Content editing templates You can use custom templates for the content editing forms. Define the templates under the `ibexa.system..content_edit_view` [configuration key](https://doc.ibexa.co/en/saas/administration/configuration/configuration/#configuration-files): ```yaml ibexa: system: default: content_edit_view: full: : template: content/edit/content_edit.html.twig match: true params: viewbaseLayout: '@ibexadesign/ui/layout.html.twig' ``` # Browsing and viewing content > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). Use PHP API to get content items and their information, content fields, location, and others. To retrieve a content item and its information, you need to make use of the [`ContentService`](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-Core-Repository-ContentService.html). The service should be [injected into the constructor of your command or controller](https://doc.ibexa.co/en/saas/api/php_api/php_api/#service-container). > **Tip: Content REST API** > > To learn how to load content items using the REST API, see [REST API reference](https://doc.ibexa.co/en/saas/api/rest_api/rest_api_reference/rest_api_reference.html#tag/Objects/operation/api_contentobjects_contentId_get). > **Tip: Console commands** > > To learn more about commands in Symfony, refer to [Console Commands](https://symfony.com/doc/7.4/console.html). ## Viewing content metadata ### ContentInfo Basic content metadata is available through [`ContentInfo`](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-Core-Repository-Values-Content-ContentInfo.html) objects and their properties. This value object provides primitive fields, such as `contentTypeId`, `publishedDate`, or `mainLocationId`, and methods for retrieving selected properties. You can also use it to request other content-related value objects from various services: ```php contentService->loadContentInfo($contentId); $output->writeln("Name: $contentInfo->name"); $output->writeln('Last modified: ' . $contentInfo->modificationDate->format('Y-m-d')); $output->writeln('Published: ' . $contentInfo->publishedDate->format('Y-m-d')); $output->writeln("RemoteId: $contentInfo->remoteId"); $output->writeln("Main Language: $contentInfo->mainLanguageCode"); $output->writeln('Always available: ' . ($contentInfo->alwaysAvailable ? 'Yes' : 'No')); return self::SUCCESS; } } ``` `ContentInfo` is loaded from the [`ContentService`](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-Core-Repository-ContentService.html) (line 13). It provides you with basic content metadata such as modification and publication dates or main language code. > **Note: Retrieving content information in a controller** > > To retrieve content information in a controller, you also make use of the `ContentService`, but rendering specific elements (for example, content information or field values) is relegated to [templates](https://doc.ibexa.co/en/saas/templating/templates/templates/index.md). ### Locations To get the locations of a content item you need to make use of the [`LocationService`](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-Core-Repository-LocationService.html): ```php $locations = $this->locationService->loadLocations($contentInfo); foreach ($locations as $location) { $output->writeln('Location: ' . $location->pathString); } ``` [`LocationService::loadLocations`](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-Core-Repository-LocationService.html#method_loadLocations) uses `ContentInfo` to get all the locations of a content item. This method returns an array of [`Location`](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-Core-Persistence-Content-Location.html) value objects. For each location, the code above prints out its `pathString` (the internal representation of the path). #### URL Aliases The [`URLAliasService`](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-Core-Repository-URLAliasService.html) additionally enables you to retrieve the human-readable [URL alias](https://doc.ibexa.co/en/saas/content_management/url_management/url_management/#url-aliases) of each location. [`URLAliasService::reverseLookup`](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-Core-Repository-URLAliasService.html#method_reverseLookup) gets the location's main [URL alias](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-Core-Repository-Values-Content-URLAlias.html): ```php $locations = $this->locationService->loadLocations($contentInfo); foreach ($locations as $location) { $urlAlias = $this->urlAliasService->reverseLookup($location); $output->writeln('URL alias: ' . $urlAlias->path); } ``` ### Content type You can retrieve the content type of a content item through the [`getContentType`](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-Core-Repository-Values-Content-ContentInfo.html#method_getContentType) method of the ContentInfo object: ```php $content = $this->contentService->loadContent($contentId); $output->writeln('Content type: ' . $content->getContentType()->getName()); ``` ### Versions To iterate over the versions of a content item, use the [`ContentService::loadVersions`](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-Core-Repository-ContentService.html#method_loadVersions) method, which returns an array of `VersionInfo` value objects. ```php $versionInfos = $this->contentService->loadVersions($contentInfo); foreach ($versionInfos as $versionInfo) { $output->write("Version $versionInfo->versionNo"); $output->write(' by ' . $versionInfo->getCreator()->getName()); $output->writeln(' in ' . $versionInfo->getInitialLanguage()->name); } ``` You can additionally provide the `loadVersions` method with the version status to get only versions of a specific status, for example: ```php $versionInfoArray = iterator_to_array($this->contentService->loadVersions($contentInfo, VersionInfo::STATUS_ARCHIVED)); ``` > **Note: Note** > > Requesting version data may be impossible for an anonymous user. Make sure to [authenticate](https://doc.ibexa.co/en/saas/api/php_api/php_api/#setting-the-repository-user) as a user with sufficient permissions. ### Relations Content Relations are versioned. To list Relations to and from your content, you can: - pass a `VersionInfo` object to the [`ContentService::loadRelationList` method](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-Core-Repository-ContentService.html#method_loadRelationList) which returns a slice of the relation list thanks to pagination arguments - use the [`RelationListIteratorAdapter`](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-Core-Repository-Iterator-BatchIteratorAdapter-RelationListIteratorAdapter.html) within a [`BatchIterator`](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-Core-Repository-Iterator-BatchIterator.html) which allow traversing the whole relation list See [Processing large result sets](https://doc.ibexa.co/en/saas/search/search_api/#process-large-result-sets) for more information about the `BatchIterator`. You can get the current version's `VersionInfo` using [`ContentService::loadVersionInfo`](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-Core-Repository-ContentService.html#method_loadVersionInfo). ```php $versionInfo = $this->contentService->loadVersionInfo($contentInfo); $relationListIterator = new BatchIterator( new RelationListIteratorAdapter( $this->contentService, $versionInfo ) ); foreach ($relationListIterator as $relationListItem) { $name = $relationListItem->hasRelation() ? $relationListItem->getRelation()->destinationContentInfo->name : '(Unauthorized)'; $output->writeln("Relation to content '$name'"); } ``` You can also specify the version number as the second argument to get Relations for a specific version: ```php /** * @var \Ibexa\Contracts\Core\Repository\Values\Content\ContentInfo $contentInfo * @var \Ibexa\Contracts\Core\Repository\ContentService $contentService */ $versionInfo = $contentService->loadVersionInfo($contentInfo, 2); ``` `loadRelationList` provides an iterable [`RelationList`](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-Core-Repository-Values-Content-RelationList.html) object listing [`Relation`](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-Core-Repository-Values-Content-Relation.html) objects. `Relation` has two main properties: `destinationContentInfo`, and `sourceContentInfo`. It also holds the [relation type](https://doc.ibexa.co/en/saas/content_management/content_relations/index.md), and the optional field this relation is made with. ### Owning user You can use the `getOwner` method of the `ContentInfo` object to load the content item's owner as a `User` value object. ```php $output->writeln('Owner: ' . $contentInfo->getOwner()->getName()); ``` To get the creator of the current version and not the content item's owner, you need to use the `creatorId` property from the current version's `VersionInfo` object. ### Section You can find the section to which a content item belongs through the [`getSection`](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-Core-Repository-Values-Content-ContentInfo.html#method_getSection) method of the ContentInfo object: ```php $output->writeln('Section: ' . $contentInfo->getSection()->name); ``` > **Note: Note** > > Requesting section data may be impossible for an anonymous user. Make sure to [authenticate](https://doc.ibexa.co/en/saas/api/php_api/php_api/#setting-the-repository-user) as a user with sufficient permissions. ### Object states You can retrieve [object states](https://doc.ibexa.co/en/saas/administration/content_organization/object_states/index.md) of a content item using [`ObjectStateService::getContentState`](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-Core-Repository-ObjectStateService.html#method_getContentState). You need to provide it with the object state group. All object state groups can be retrieved through [`loadObjectStateGroups`](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-Core-Repository-ObjectStateService.html#method_loadObjectStateGroups). ```php $stateGroups = $this->objectStateService->loadObjectStateGroups(); foreach ($stateGroups as $stateGroup) { $state = $this->objectStateService->getContentState($contentInfo, $stateGroup); $output->writeln("Object state: $state->identifier"); } ``` ## Viewing field definitions of content types To retrieve the content type's field definitions of a selected content item, you can use the following command: ```php getArgument('contentId'); $content = $this->contentService->loadContent($contentId); $contentType = $this->contentTypeService->loadContentType($content->contentInfo->contentTypeId); foreach ($contentType->fieldDefinitions as $fieldDefinition) { $output->writeln('Field: ' . $fieldDefinition->identifier); $fieldType = $this->fieldTypeService->getFieldType($fieldDefinition->fieldTypeIdentifier); $field = $content->getFieldValue($fieldDefinition->identifier); $valueHash = $fieldType->toHash($field); $output->writeln('Value:'); $output->writeln($valueHash); } return self::SUCCESS; } } ``` Line 17 shows how [`ContentService::loadContent`](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-Core-Repository-ContentService.html#method_loadContent) loads the content item provided to the command. Line 18 makes use of the [`ContentTypeService`](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-Core-Repository-ContentTypeService.html) to retrieve the content type of the requested item. Lines 20-27 iterate over fields defined by the content type. For each field definition they print out its identifier, and then using [`FieldTypeService`](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-Core-Repository-FieldTypeService.html) retrieve the field definition's value and print it out to the console. ## Viewing content in different languages The repository is SiteAccess-aware, so languages defined by the SiteAccess are automatically taken into account when loading content. To load a specific language, provide its language code when loading the content item: ```php /** * @var int $contentId * @var \Ibexa\Contracts\Core\Repository\ContentService $contentService */ $content = $contentService->loadContent($contentId, ['ger-DE']); ``` To load all languages as a prioritized list, use `Language::ALL`: ```php use Ibexa\Contracts\Core\Repository\Values\Content\Language; /** * @var \Ibexa\Contracts\Core\Repository\ContentService $contentService * @var \Ibexa\Contracts\Core\Repository\Values\Content\Content $content */ $contentService->loadContent($content->id, Language::ALL); ``` ## Getting all content in a subtree To go through all the content items contained in a subtree, you need to use the [`LocationService`](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-Core-Repository-LocationService.html). ```php private function browseLocation(Location $location, OutputInterface $output, int $depth = 0): void { $output->writeln($location->contentInfo->name); $children = $this->locationService->loadLocationChildren($location); foreach ($children->locations as $child) { $this->browseLocation($child, $output, $depth + 1); } } protected function execute(InputInterface $input, OutputInterface $output): int { $locationId = (int) $input->getArgument('locationId'); $location = $this->locationService->loadLocation($locationId); $this->browseLocation($location, $output); return self::SUCCESS; } ``` `loadLocation` (line 15) returns a value object, here a `Location`. [`LocationService::loadLocationChildren`](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-Core-Repository-LocationService.html#method_loadLocationChildren) (line 5) returns a [`LocationList`](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-Core-Repository-Values-Content-LocationList.html) value object that you can iterate over. > **Note: Note** > > Refer to [Searching](https://doc.ibexa.co/en/saas/search/search_api/index.md) for information on more complex search queries. ## Getting parent location To get the parent location of content, you first need to determine which location is the main one, in case the content item has multiple locations. You can do it through the `getMainLocation` method of the ContentInfo object. Next, use the `getParentLocation` method of the location object to access the parent location: ```php /** @var \Ibexa\Contracts\Core\Repository\Values\Content\ContentInfo $contentInfo */ $mainLocation = $contentInfo->getMainLocation(); $parentLocation = $mainLocation?->getParentLocation(); if ($parentLocation !== null) { $message = 'Parent Location: ' . $parentLocation->pathString; } ``` ## Getting content from a location When dealing with location objects (and Trash objects), you can get access to content item directly using `$location->getContent`. In Twig this can also be accessed by `location.content`. This is a lazy property. It triggers loading of content when first used. In case of bulk of locations coming from Search or location Service, the content is also loaded in bulk for the whole location result set. ## Comparing content versions You can compare two versions of a content item using the `VersionComparisonService`. The versions must have the same language. For example, to get the comparison between the `name` field of two versions: ```php /** * @var \Ibexa\Contracts\Core\Repository\Values\Content\ContentInfo $contentInfo * @var int $versionFromId * @var int $versionToId * @var \Ibexa\Contracts\Core\Repository\ContentService $contentService * @var \Ibexa\Contracts\VersionComparison\Service\VersionComparisonServiceInterface $comparisonService */ $versionFrom = $contentService->loadVersionInfo($contentInfo, $versionFromId); $versionTo = $contentService->loadVersionInfo($contentInfo, $versionToId); $nameComparison = $comparisonService->compare($versionFrom, $versionTo)->getFieldValueDiffByIdentifier('name')->getComparisonResult(); ``` `getComparisonResult` returns a `ComparisonResult` object, which depends on the field type being compared. In the example of a Text Line (ibexa_string) field, it's an array of `StringDiff` objects. Each diff contains a section of the field to compare (for example, a part of a text line) and its status, which can be "unchanged", "added" or "removed". # Creating content > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). Create, publish, update and translate content items by using the PHP API. > **Note: Note** > > Creating most objects is impossible for an anonymous user. Make sure to [authenticate](https://doc.ibexa.co/en/saas/api/php_api/php_api/#setting-the-repository-user) as a user with sufficient permissions. > **Tip: Content REST API** > > To learn how to create content items using the REST API, see [REST API reference](https://doc.ibexa.co/en/saas/api/rest_api/rest_api_reference/rest_api_reference.html#tag/Objects/operation/api_contentobjects_post). ## Creating content item draft Value objects such as content items are read-only, so to create or modify them you need to use structs. [`ContentService::newContentCreateStruct`](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-Core-Repository-ContentService.html#method_newContentCreateStruct) returns a new [`ContentCreateStruct`](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-Core-Repository-Values-Content-ContentCreateStruct.html) object. ```php $contentType = $this->contentTypeService->loadContentTypeByIdentifier($contentTypeIdentifier); $contentCreateStruct = $this->contentService->newContentCreateStruct($contentType, 'eng-GB'); $contentCreateStruct->setField('name', $name); $locationCreateStruct = $this->locationService->newLocationCreateStruct($parentLocationId); $draft = $this->contentService->createContent($contentCreateStruct, [$locationCreateStruct]); $output->writeln('Created a draft of ' . $contentType->getName() . ' with name ' . $draft->getName()); ``` This command creates a draft using [`ContentService::createContent`](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-Core-Repository-ContentService.html#method_createContent) (line 6). This method must receive a `ContentCreateStruct` and an array of location structs. `ContentCreateStruct` (which extends `ContentStruct`) is created through [`ContentService::newContentCreateStruct`](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-Core-Repository-ContentService.html#method_newContentCreateStruct) (line 1), which receives the content type and the primary language for the content item. For information about translating a content item into other languages, see [Translating content](#translating-content). [`ContentStruct::setField`](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-Core-Repository-Values-Content-ContentStruct.html#method_setField) (line 2) enables you to define the field values. When the field accepts a simple value, you can provide it directly, as in the example above. For some field types, for example [images](#creating-an-image), you need to provide an instance of a Value type. ### Creating an image Image field type requires an instance of its Value type, which you must provide to the [`ContentStruct::setField`](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-Core-Repository-Values-Content-ContentStruct.html#method_setField) method. Therefore, when creating a content item of the Image type (or any other content type with an `image` field type), the `ContentCreateStruct` is slightly more complex than in the previous example: ```php $contentType = $this->contentTypeService->loadContentTypeByIdentifier('image'); $contentCreateStruct = $this->contentService->newContentCreateStruct($contentType, 'eng-GB'); $contentCreateStruct->setField('name', $name); $imageValue = new Value( [ 'path' => $file, 'fileSize' => filesize($file), 'fileName' => basename((string) $file), 'alternativeText' => $name, ] ); $contentCreateStruct->setField('image', $imageValue); ``` Value of the Image field type contains the path to the image file and other basic information based on the input file. ### Creating content with RichText The RichText field accepts values in a custom flavor of [DocBook](https://github.com/docbook/wiki/wiki) format. For example, to add a RichText paragraph, provide the following as input: ```xml
    Description of your content item.
    ``` To learn more about the format and how it represents different elements of rich text, see [RichText field type reference](https://doc.ibexa.co/en/saas/content_management/field_types/field_type_reference/richtextfield/#custom-docbook-format). ## Publishing a draft [`ContentService::createContent`](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-Core-Repository-ContentService.html#method_createContent) creates a content item with only one draft version. To publish it, use [`ContentService::publishVersion`](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-Core-Repository-ContentService.html#method_publishVersion). This method must get the [`VersionInfo`](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-Core-Repository-Values-Content-VersionInfo.html) object of a draft version. ```php $content = $this->contentService->publishVersion($draft->versionInfo); ``` ## Updating content To update an existing content item, you need to prepare a [`ContentUpdateStruct`](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-Core-Repository-Values-Content-ContentUpdateStruct.html) and pass it to [`ContentService::updateContent`](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-Core-Repository-ContentService.html#method_updateContent). This method works on a draft, so to publish your changes you need to use [`ContentService::publishVersion`](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-Core-Repository-ContentService.html#method_publishVersion) as well: ```php $contentInfo = $this->contentService->loadContentInfo($contentId); $contentDraft = $this->contentService->createContentDraft($contentInfo); $contentUpdateStruct = $this->contentService->newContentUpdateStruct(); $contentUpdateStruct->initialLanguageCode = 'eng-GB'; $contentUpdateStruct->setField('name', $newName); $contentDraft = $this->contentService->updateContent($contentDraft->versionInfo, $contentUpdateStruct); $this->contentService->publishVersion($contentDraft->versionInfo); ``` ## Translating content Content [translations](https://doc.ibexa.co/en/saas/multisite/languages/languages/#language-versions) are created per version. By default every version contains all existing translations. To translate a content item to a new language, you need to update it and provide a new `initialLanguageCode`: ```php $contentInfo = $this->contentService->loadContentInfo($contentId); $contentDraft = $this->contentService->createContentDraft($contentInfo); $contentUpdateStruct = $this->contentService->newContentUpdateStruct(); $contentUpdateStruct->initialLanguageCode = $language; $contentUpdateStruct->setField('name', $newName); $contentDraft = $this->contentService->updateContent($contentDraft->versionInfo, $contentUpdateStruct); $this->contentService->publishVersion($contentDraft->versionInfo); ``` You can also update content in multiple languages at once using the `setField` method's third argument. Only one language can still be set as a version's initial language: ```php $contentUpdateStruct->setField('name', $nameInSecondaryLanguage, $secondaryLanguage); ``` ### Deleting a translation You can delete a single translation from a content item's version using [`ContentService::deleteTranslationFromDraft`](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-Core-Repository-ContentService.html#method_deleteTranslationFromDraft). The method must be provided with a `VersionInfo` object and the code of the language to delete: ```php /** @var \Ibexa\Contracts\Core\Repository\Values\Content\VersionInfo $versionInfo */ $languageCode = 'ger-DE'; /** @var \Ibexa\Contracts\Core\Repository\ContentService $contentService */ $contentService->deleteTranslationFromDraft($versionInfo, $languageCode); ``` # Managing content > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). PHP API enables managing content Locations, content types, content in Trash, and Calendar events. ## Locations You can manage [locations](https://doc.ibexa.co/en/saas/content_management/locations/index.md) that hold content using [`LocationService`](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-Core-Repository-LocationService.html). > **Tip: Location REST API** > > To learn how to manage locations using the REST API, see [REST API reference](https://doc.ibexa.co/en/saas/api/rest_api/rest_api_reference/rest_api_reference.html#tag/Objects/operation/api_contentobjects_contentIdlocations_post). ### Adding a new location to a content item Every published content item must have at least one location. One content item can have more that one location, which means it's presented in more than one place in the content tree. Creating a new location, like creating content, requires using a struct, because a location value object is read-only. To add a new location to existing content you need to create a [`LocationCreateStruct`](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-Core-Repository-Values-Content-LocationCreateStruct.html) and pass it to the [`LocationService::createLocation`](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-Core-Repository-LocationService.html#method_createLocation) method: ```php $locationCreateStruct = $this->locationService->newLocationCreateStruct($parentLocationId); $contentInfo = $this->contentService->loadContentInfo($contentId); $newLocation = $this->locationService->createLocation($contentInfo, $locationCreateStruct); ``` `LocationCreateStruct` must receive the parent location ID. It sets the `parentLocationId` property of the new location. You can also provide other properties for the location, otherwise they're set to their defaults: ```php $locationCreateStruct->priority = 500; $locationCreateStruct->hidden = true; ``` ### Changing the main location When a content item has more that one location, one location is always considered the main one. You can change the main location using [`ContentService`](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-Core-Repository-ContentService.html), by updating the `ContentInfo` with a [`ContentUpdateStruct`](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-Core-Repository-Values-Content-ContentUpdateStruct.html) that sets the new main location: ```php $contentUpdateStruct = $this->contentService->newContentMetadataUpdateStruct(); $contentUpdateStruct->mainLocationId = $locationId; $this->contentService->updateContentMetadata($contentInfo, $contentUpdateStruct); ``` ### Hiding and revealing locations To hide or reveal (unhide) a location you need to make use of [`LocationService::hideLocation`](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-Core-Repository-LocationService.html#method_hideLocation) or [`LocationService::unhideLocation`:](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-Core-Repository-LocationService.html#method_unhideLocation) ```php $this->locationService->hideLocation($location); $this->locationService->unhideLocation($location); ``` See [location visibility](https://doc.ibexa.co/en/saas/content_management/locations/#location-visibility) for detailed information on the behavior of visible and hidden Locations. ### Deleting a location You can remove a location either by deleting it, or sending it to Trash. Deleting makes use of [`LocationService::deleteLocation()`](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-Core-Repository-LocationService.html#method_deleteLocation). It permanently deletes the location, together with its whole subtree. Content which has only this one location is permanently deleted as well. Content which has more locations is still available in its other locations. If you delete the [main location](#changing-the-main-location) of a content item that has more locations, another location becomes the main one. ```php $location = $this->locationService->loadLocation($locationId); $this->locationService->deleteLocation($location); ``` To send the location and its subtree to Trash, use [`TrashService::trash`](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-Core-Repository-TrashService.html#method_trash). Items in Trash can be later [restored, or deleted permanently](#trash). ```php $this->trashService->trash($location); ``` ### Moving and copying a subtree You can move a location with its whole subtree using [`LocationService::moveSubtree`](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-Core-Repository-LocationService.html#method_moveSubtree): ```php $sourceLocation = $this->locationService->loadLocation($locationId); $targetLocation = $this->locationService->loadLocation($targetLocationId); $this->locationService->moveSubtree($sourceLocation, $targetLocation); ``` [`LocationService::copySubtree`](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-Core-Repository-LocationService.html#method_copySubtree) is used in the same way, but it copies the location and its subtree instead of moving it. > **Tip: Tip** > > To copy a subtree you can also make use of the built-in `copy-subtree` command: `bin/console ibexa:copy-subtree `. > **Note: Note** > > [Copy subtree limit](https://doc.ibexa.co/en/saas/administration/back_office/back_office_configuration/#copy-subtree-limit) only applies to operations in the back office. It's ignored when copying subtrees using the PHP API. ## Trash > **Tip: Trash REST API** > > To learn how to manage Trash using the REST API, see [REST API reference](https://doc.ibexa.co/en/saas/api/rest_api/rest_api_reference/rest_api_reference.html#tag/Trash). To empty the Trash (remove all locations in Trash), use [`TrashService::emptyTrash`](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-Core-Repository-TrashService.html#method_emptyTrash), which takes no arguments. You can recover an item from Trash using [`TrashService::recover`](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-Core-Repository-TrashService.html#method_recover). You must provide the method with the ID of the object in Trash. Trash location is identical to the origin location of the object. ```php $this->trashService->recover($trashItem, $newParent); ``` The content item is restored under its previous location. You can also provide a different location to restore in as a second argument: ```php /** * @var \Ibexa\Contracts\Core\Repository\Values\Content\TrashItem $trashItem * @var \Ibexa\Contracts\Core\Repository\LocationService $locationService * @var \Ibexa\Contracts\Core\Repository\TrashService $trashService */ $locationId = 12345; $newParent = $locationService->loadLocation($locationId); $trashService->recover($trashItem, $newParent); ``` You can also search through Trash items and sort the results using several public PHP API Search Criteria and Sort Clauses that have been exposed for `TrashService` queries. For more information, see [Search in trash](https://doc.ibexa.co/en/saas/search/search_api/#search-in-trash). ## Content types > **Tip: Content type REST API** > > To learn how to manage content types using the REST API, see REST API reference for [content types](https://doc.ibexa.co/en/saas/api/rest_api/rest_api_reference/rest_api_reference.html#tag/Type) and [content type groups](https://doc.ibexa.co/en/saas/api/rest_api/rest_api_reference/rest_api_reference.html#tag/Type-Groups). ### Adding content types To operate on content types, you need to make use of [`ContentTypeService`](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-Core-Repository-ContentTypeService.html). Adding a new content type, like creating content, must happen with the use of a struct, because a content type value object is read-only. In this case you use [`ContentTypeCreateStruct`](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-Core-Repository-Values-ContentType-ContentTypeCreateStruct.html). A content type must have at least one name, in the main language, and at least one field definition. ```php $contentTypeCreateStruct = $this->contentTypeService->newContentTypeCreateStruct($contentTypeIdentifier); $contentTypeCreateStruct->mainLanguageCode = 'eng-GB'; $contentTypeCreateStruct->nameSchema = ''; $contentTypeCreateStruct->names = [ 'eng-GB' => $contentTypeIdentifier, ]; $titleFieldCreateStruct = $this->contentTypeService->newFieldDefinitionCreateStruct('name', 'ibexa_string'); $contentTypeCreateStruct->addFieldDefinition($titleFieldCreateStruct); $contentTypeDraft = $this->contentTypeService->createContentType( $contentTypeCreateStruct, [$contentTypeGroup] ); $this->contentTypeService->publishContentTypeDraft($contentTypeDraft); ``` You can specify more details of the field definition in the create struct, for example: ```php $titleFieldCreateStruct = $this->contentTypeService->newFieldDefinitionCreateStruct('name', 'ibexa_string'); $titleFieldCreateStruct->names = ['eng-GB' => 'Name']; $titleFieldCreateStruct->descriptions = ['eng-GB' => 'The name']; $titleFieldCreateStruct->fieldGroup = 'content'; $titleFieldCreateStruct->position = 10; $titleFieldCreateStruct->isTranslatable = true; $titleFieldCreateStruct->isRequired = true; $titleFieldCreateStruct->isSearchable = true; ``` ### Copying content types To copy a content type, use [`ContentTypeService::copyContentType`](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-Core-Repository-ContentTypeService.html#method_copyContentType): ```php $contentTypeToCopy = $this->contentTypeService->loadContentTypeByIdentifier($contentTypeIdentifier); $copy = $this->contentTypeService->copyContentType($contentTypeToCopy); ``` The copy is automatically getting an identifier based on the original content type identifier and the copy's ID, for example: `copy_of_folder_21`. To change the identifier of the copy, use a [`ContentTypeUpdateStruct`](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-Core-Repository-Values-ContentType-ContentTypeUpdateStruct.html): ```php $copy = $this->contentTypeService->copyContentType($contentTypeToCopy); $copyDraft = $this->contentTypeService->createContentTypeDraft($copy); $copyUpdateStruct = $this->contentTypeService->newContentTypeUpdateStruct(); $copyUpdateStruct->identifier = $copyIdentifier; $copyUpdateStruct->names = ['eng-GB' => $copyIdentifier]; $this->contentTypeService->updateContentTypeDraft($copyDraft, $copyUpdateStruct); ``` ### Finding and filtering content types You can find content types that match specific criteria by using the [`ContentTypeService::findContentTypes()`](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-Core-Repository-ContentTypeService.html#method_findContentTypes) method. This method accepts a `ContentTypeQuery` object that supports filtering and sorting by IDs, identifiers, group membership, and other criteria. > **Note: Criteria, sort clauses and REST APIs** > > For a full list of available criteria and sort clauses that you can use when finding and filtering content types, see [Content Type Search Criteria](https://doc.ibexa.co/en/saas/search/content_type_search_reference/content_type_criteria/index.md) and [Content Type Search Sort Clauses](https://doc.ibexa.co/en/saas/search/content_type_search_reference/content_type_sort_clauses/index.md) references. > > For the REST API, see [Filter content types](https://doc.ibexa.co/en/saas/api/rest_api/rest_api_reference/rest_api_reference.html#tag/Type/operation/api_contenttypesview_post). The following example shows how you can use the criteria to find content types: ```php contentTypeService->findContentTypes($query); $output->writeln('Found ' . $searchResult->getTotalCount() . ' content type(s):'); foreach ($searchResult->getContentTypes() as $contentType) { $output->writeln(sprintf( '- [%d] %s (identifier: %s)', $contentType->id, $contentType->getName(), $contentType->identifier )); } return Command::SUCCESS; } } ``` #### Query parameters When constructing a `ContentTypeQuery`, you can pass the following parameters: - `?CriterionInterface $criterion = null` — a filter to apply (use one or a combination of the criteria above) - `array $sortClauses = []` — list of sort clauses to order the results - `int $offset = 0` — starting offset (for pagination) - `int $limit = 25` — maximum number of results to return ## Calendar events You can handle the calendar using `CalendarServiceInterface` (`Ibexa\Contracts\Calendar\CalendarServiceInterface`). > **Tip: Calendar REST API** > > To learn how to manage the Calendar using the REST API, see [REST API reference](https://doc.ibexa.co/en/saas/api/rest_api/rest_api_reference/rest_api_reference.html#tag/Calendar). ### Getting events To get a list of events for a specified time period, use the `CalendarServiceInterface::getEvents` method. You need to provide the method with an EventQuery, which takes a date range and a count as the minimum of parameters: ```php $dateFrom = new \DateTimeImmutable('2023-01-01T10:00:00+00:00'); $dateTo = new \DateTimeImmutable('2023-12-31T10:0:00+00:00'); $dateRange = new Calendar\DateRange($dateFrom, $dateTo); $eventQuery = new Calendar\EventQuery($dateRange, 10); $eventList = $this->calendarService->getEvents($eventQuery); foreach ($eventList as $event) { $output->writeln($event->getName() . '; date: ' . $event->getDateTime()->format('T Y-m-d H:i:s')); } ``` You can also get the first and last event in the list by using the `first()` and `last()` methods of an `EventCollection` (`Ibexa\Contracts\Calendar\EventCollection`): ```php $eventCollection = $eventList->getEvents(); $output->writeln('First event: ' . $eventCollection->first()->getName() . '; date: ' . $eventCollection->first()->getDateTime()->format('T Y-m-d H:i:s')); ``` You can process the events in a collection using the `find(Closure $predicate)`, `filter(Closure $predicate)`, `map(Closure $callback)` or `slice(int $offset, ?int $length = null)` methods of `EventCollection`, for example: ```php $newCollection = $eventCollection->slice(3, 5); foreach ($newCollection as $event) { $output->writeln('New collection: ' . $event->getName() . '; date: ' . $event->getDateTime()->format('T Y-m-d H:i:s')); } ``` ### Performing calendar actions You can perform a calendar action (for example, reschedule or unschedule calendar events) using the `CalendarServiceInterface::executeAction()` method. You must pass an `Ibexa\Contracts\Calendar\EventAction\EventActionContext` instance as argument. `EventActionContext` defines events on which the action is performed, and action-specific parameters, for example, a new date: ```php $newDate = new \DateTimeImmutable('2023-12-06T13:00:00+00:00'); $context = new RescheduleEventActionContext($eventCollection, $newDate); $this->calendarService->executeAction($context); ``` # Bookmark API > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). You can use the PHP API to view the bookmark list, and add or remove content from it. [`BookmarkService`](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-Core-Repository-BookmarkService.html) enables you to read, add and remove bookmarks from content. > **Tip: Bookmark REST API** > > To learn how to manage bookmarks using the REST API, see [REST API reference](https://doc.ibexa.co/en/saas/api/rest_api/rest_api_reference/rest_api_reference.html#tag/Bookmark). To view a list of all bookmarks, use [`BookmarkService::loadBookmarks`](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-Core-Repository-BookmarkService.html#method_loadBookmarks): ```php $bookmarkList = $this->bookmarkService->loadBookmarks(); $output->writeln('Total bookmarks: ' . $bookmarkList->totalCount); foreach ($bookmarkList->items as $bookmark) { $output->writeln($bookmark->getContentInfo()->name); } ``` You can add a bookmark to a content item by providing its Location object to the [`BookmarkService::createBookmark`](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-Core-Repository-BookmarkService.html#method_createBookmark) method: ```php $location = $this->locationService->loadLocation($locationId); $this->bookmarkService->createBookmark($location); ``` You can remove a bookmark from a location with [`BookmarkService::deleteBookmark`](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-Core-Repository-BookmarkService.html#method_deleteBookmark): ```php $this->bookmarkService->deleteBookmark($location); ``` # Section API > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). PHP API enables you to create sections, assign content to them, and get various information about the section. [Sections](https://doc.ibexa.co/en/saas/administration/content_organization/sections/index.md) enable you to divide content into groups which can later be used, for example, as basis for permissions. You can manage sections by using the PHP API by using `SectionService`. > **Tip: Section REST API** > > To learn how to manage sections using the REST API, see [REST API reference](https://doc.ibexa.co/en/saas/api/rest_api/rest_api_reference/rest_api_reference.html#tag/Section). ## Creating sections To create a new section, you need to make use of the [`SectionCreateStruct`](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-Core-Repository-Values-Content-SectionCreateStruct.html) and pass it to the [`SectionService::createSection`](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-Core-Repository-SectionService.html#method_createSection) method: ```php $sectionCreateStruct = $this->sectionService->newSectionCreateStruct(); $sectionCreateStruct->name = $sectionName; $sectionCreateStruct->identifier = $sectionIdentifier; $this->sectionService->createSection($sectionCreateStruct); ``` ## Getting section information You can use `SectionService` to retrieve section information such as whether it's in use: ```php $output->writeln(( $this->sectionService->isSectionUsed($section) ? 'This section is in use.' : 'This section is not in use.' )); ``` ## Listing content in a section To list content items assigned to a section you need to make a [query](https://doc.ibexa.co/en/saas/search/search_api/index.md) for content belonging to this section, by applying the [`SearchService`](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-Core-Repository-SearchService.html). You can also use the query to get the total number of assigned content items: ```php $query = new LocationQuery(); $query->filter = new Criterion\SectionId([ $section->id, ]); $result = $this->searchService->findContentInfo($query); foreach ($result->searchHits as $searchResult) { $output->writeln('* ' . $searchResult->valueObject->name); } ``` ## Assigning section to content To assign content to a section, use the [`SectionService::assignSection`](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-Core-Repository-SectionService.html#method_assignSection) method. You need to provide it with the `ContentInfo` object of the content item, and the [`Section`](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-Core-Repository-Values-Content-Section.html) object: ```php $section = $this->sectionService->loadSectionByIdentifier($sectionIdentifier); $contentInfo = $this->contentService->loadContentInfo($contentId); $this->sectionService->assignSection($contentInfo, $section); ``` Assigning a section to content doesn't automatically assign it to the content item's children. # Object state API > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). You can manage object states via the PHP API, including creating object states and state groups and assigning them to content items. [Object states](https://doc.ibexa.co/en/saas/administration/content_organization/object_states/index.md) enable you to set a custom state to any content. States are grouped into object state groups. You can manage Object states by using the PHP API by using `ObjectStateService`. > **Tip: Object state REST API** > > To learn how to manage object states using the REST API, see [REST API reference](https://doc.ibexa.co/en/saas/api/rest_api/rest_api_reference/rest_api_reference.html#tag/Objects/operation/api_contentobjects_contentIdobjectstates_get). ## Getting object state information You can use the [`ObjectStateService`](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-Core-Repository-ObjectStateService.html) to get information about object state groups or object states. ```php $objectStateGroup = $this->objectStateService->loadObjectStateGroupByIdentifier('ibexa_lock'); $objectState = $this->objectStateService->loadObjectStateByIdentifier($objectStateGroup, 'locked'); $output->writeln($objectStateGroup->getName()); $output->writeln($objectState->getName()); ``` ## Creating object states To create an object state group and add object states to it, you need to make use of the [`ObjectStateService`](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-Core-Repository-ObjectStateService.html): ```php $objectStateGroupStruct = $this->objectStateService->newObjectStateGroupCreateStruct($objectStateGroupIdentifier); $objectStateGroupStruct->defaultLanguageCode = 'eng-GB'; $objectStateGroupStruct->names = ['eng-GB' => $objectStateGroupIdentifier]; $newObjectStateGroup = $this->objectStateService->createObjectStateGroup($objectStateGroupStruct); ``` [`ObjectStateService::createObjectStateGroup`](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-Core-Repository-ObjectStateService.html#method_createObjectStateGroup) takes as argument an [`ObjectStateGroupCreateStruct`](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-Core-Repository-Values-ObjectState-ObjectStateGroupCreateStruct.html), in which you need to specify the identifier, default language and at least one name for the group. To create an object state inside a group, use [`ObjectStateService::newObjectStateCreateStruct`](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-Core-Repository-ObjectStateService.html#method_newObjectStateCreateStruct) and provide it with an `ObjectStateCreateStruct`: ```php $stateStruct = $this->objectStateService->newObjectStateCreateStruct($objectStateIdentifier); $stateStruct->defaultLanguageCode = 'eng-GB'; $stateStruct->names = ['eng-GB' => $objectStateIdentifier]; $this->objectStateService->createObjectState($newObjectStateGroup, $stateStruct); ``` ## Assigning object state To assign an object state to a content item, use [`ObjectStateService::setContentState`](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-Core-Repository-ObjectStateService.html#method_setContentState). Provide it with a `ContentInfo` object of the content item, the object state group and the object state: ```php $contentInfo = $this->contentService->loadContentInfo($contentId); $objectStateGroup = $this->objectStateService->loadObjectStateGroupByIdentifier($objectStateGroupIdentifier); $objectState = $this->objectStateService->loadObjectStateByIdentifier($objectStateGroup, $objectStateToAssign); $this->objectStateService->setContentState($contentInfo, $objectStateGroup, $objectState); ``` # Data migration > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). Data migration enables you to import and export repository data by using YAML files. Data migration allows exporting and importing selected data from a Cohesivo installation. [*Exporting*](https://doc.ibexa.co/en/saas/content_management/data_migration/exporting_data/index.md) data consists in saving selected repository information in YAML format. [*Importing*](https://doc.ibexa.co/en/saas/content_management/data_migration/importing_data/index.md) reads migration YAML files and creates or modifies repository content based on them. Between installation, you can migrate your repository data, for example, content items, content types, languages, object states, or sections. You can use migrations in projects that require the same data to be present across multiple instances. You can use them for project templates. Migrations are able to store shared data, so they can be applied for each new project you start, or incrementally upgrade older projects to your new standard, if needed. They're a developer-friendly tool that allows you to share data without writing code. You can run data migrations either with a command, or with the [PHP API](https://doc.ibexa.co/en/saas/content_management/data_migration/data_migration_api/index.md). - [Importing data](https://ez-systems-developer-documentation--3381.com.readthedocs.build/en/3381/content_management/data_migration/importing_data/): Import data into your repository from prepared YAML files. - [Exporting data](https://ez-systems-developer-documentation--3381.com.readthedocs.build/en/3381/content_management/data_migration/exporting_data/): Export repository data to use in future data migrations. - [Data migration actions](https://ez-systems-developer-documentation--3381.com.readthedocs.build/en/3381/content_management/data_migration/data_migration_actions/): Data migration actions enable you to run special operations while executing data migrations, such as assigning roles, sections, Objects states, and more. - [Managing migrations](https://ez-systems-developer-documentation--3381.com.readthedocs.build/en/3381/content_management/data_migration/managing_migrations/): Manage data migrations by adding files, converting from Kaliop migration bundle, checking migration status, and setting up configuration. # Importing data > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). Import data into your repository from prepared YAML files. To import data from YAML migration files into repository, you run the `ibexa:migrations:migrate` command. The `ibexa:migrations:import` command automatically places migration files in the correct folder. Alternatively, you can place the files manually in the `src/Migrations/Ibexa/migrations` folder or in [a custom folder that you configure](https://doc.ibexa.co/en/saas/content_management/data_migration/managing_migrations/#migration-folders), and specify the file name within this folder as parameter. If you don't specify the file, all files within this directory are used. ```bash php bin/console ibexa:migrations:migrate --file=my_data_export.yaml --siteaccess=admin ``` Migrations store execution metadata in the `ibexa_migrations` database table. This allows incremental upgrades: the `ibexa:migrations:migrate` command ignores files that it had previously executed. The [`--siteaccess` option](https://doc.ibexa.co/en/saas/content_management/data_migration/exporting_data/#siteaccess) usage can be relevant when multiple languages or multiple repositories are used. ## Migration step A data migration step is a single operation in data migration process that combines a mode (for example: `create`, `update`, `delete`) and a type (for example: `content`, `section`, `currency`), with optional additional information depending on the specific step. In a migration file, a step is an array item starting with the mandatory properties `type` and `mode`, for example: ```yaml - type: content mode: create ``` Then, the step is described by additional properties depending on its type and mode. - See [Available migrations](#available-migrations) for the modes available for each type. - See [Migration examples](#migration-examples) to explore what you can do with each type. - For a custom migration step, see [Create data migration step](https://doc.ibexa.co/en/saas/content_management/data_migration/create_data_migration_step/index.md). ## Available migrations The following data migration step modes are available: | `type` | `create` | `update` | `delete` | `swap` | `trash` | | ---------------------- | -------- | -------- | -------- | ------ | ------- | | `action_configuration` | Yes | Yes | Yes | | | | `attribute` | Yes | Yes | Yes | | | | `attribute_group` | Yes | Yes | Yes | | | | `company` | Yes | | | | | | `content_type` | Yes | Yes | Yes | | | | `content_type_group` | Yes | Yes | Yes | | | | `content` | Yes | Yes | Yes | | | | `currency` | Yes | Yes | Yes | | | | `customer_group` | Yes | Yes | Yes | | | | `language` | Yes | Yes | | | | | `location` | | Yes | | Yes | Yes | | `object_state` | Yes | | | | | | `object_state_group` | Yes | | | | | | `product_asset` | Yes | | | | | | `product_availability` | Yes | | | | | | `product_price` | Yes | | | | | | `product_variant` | Yes | | | | | | `role` | Yes | Yes | Yes | | | | `section` | Yes | Yes | | | | | `segment` | Yes | Yes | Yes | | | | `segment_group` | Yes | Yes | Yes | | | | `setting` | Yes | Yes | Yes | | | | `user` | Yes | Yes | | | | | `user_group` | Yes | Yes | Yes | | | Additionally, the following special migration types are available: | `type` | `mode` | | -------------- | ----------------------------- | | `reference` | `load`, `save`, `set`, `list` | | `repeatable` | `create` | | `service_call` | `execute` | | `sql` | `execute` | | `try_catch` | `execute` | For more information about the `reference` type, see [References](https://doc.ibexa.co/en/saas/content_management/data_migration/managing_migrations/#references). ### Repeatable steps You can run a set of one or more similar migration steps multiple times by using the special `repeatable` migration type. A repeatable migration performs the defined migration steps as many times as specified: - with an [iteration counter](#repeatable-steps-with-iteration-counter), mimicking the behavior of a [`for` loop](https://www.php.net/manual/en/control-structures.for.php) - with a [list of items](#repeatable-steps-with-items), mimicking the behavior of a [`foreach` loop](https://www.php.net/manual/en/control-structures.foreach.php) > **Tip: Tip** > > You can use repeatable migration steps, for example, to quickly generate large numbers of content items for testing purposes. #### Repeatable steps with iteration counter You can vary the operations with the iteration counter. For example, to create five Folders, with names ranging from "Folder 0" to "Folder 4", you can run the following migration using the iteration counter `i`: ```yaml - type: repeatable mode: create iterations: 5 steps: - type: content mode: create metadata: contentType: folder mainTranslation: eng-GB location: parentLocationId: 2 fields: - fieldDefIdentifier: name languageCode: eng-GB value: 'Folder ###XXX i XXX###' ``` To vary the content name, the migration above uses [Symfony expression syntax](#expression-syntax). In the example above, the expression is enclosed in `###` and the repeated string `XXX`. > **Note: Note** > > Iteration counter is assigned to `i` by default, but you can modify it in the `iteration_counter_name` setting. The counter starts at `0` by default. You can change this with the `iteration_counter_starting_value` setting. #### Repeatable steps with items By using the `items` key, you can provide an array of items to the `repeatable` step: ```yaml - type: repeatable mode: create steps: - type: language mode: create metadata: languageCode: '###XXX code XXX###' name: '###XXX name XXX###' enabled: true items: - { code: afr-AF, name: Afrikaans } - { code: alb-SQ, name: Albanian } - { code: ara-AR, name: Arabic } ``` In the example above, the step runs for each entry declared in `items`. On each run, the values of `code` and `name` keys are available as variables. The iteration counter variable (named `i` by default) is also available and holds the zero-based index of the current item. You can rename it with the `iteration_counter_name` setting and combine it with item properties as in the following example: ```yaml - type: repeatable mode: create iteration_counter_name: index steps: - type: content mode: create metadata: contentType: folder mainTranslation: eng-GB remoteId: 'migration_folder_###XXX index XXX###' location: parentLocationId: 2 fields: - fieldDefIdentifier: name languageCode: eng-GB value: '###XXX title XXX###' items: - { title: 'Getting Started' } - { title: 'Advanced Configuration' } - { title: 'API Reference' } ``` This migration results in three new content items: | Content item name | Remote location ID | | ---------------------- | --------------------- | | Getting started | `migration_article_0` | | Advanced Configuration | `migration_article_1` | | API Reference | `migration_article_2` | #### Generating fake data You can also generate fake data with the help of [`FakerPHP`](https://fakerphp.org/). To use it, first install Faker on your system: ```bash composer require fakerphp/faker ``` Then, you can use `faker()` in expressions, for example: ```yaml - fieldDefIdentifier: short_name languageCode: eng-GB value: '### faker().name() ###' ``` This step generates field values with fake personal names. ### SQL migrations You can execute raw SQL queries directly in migrations by using the `sql` migration type. Use it for custom database operations that don't fit into standard entity migrations, such as creating custom tables or performing bulk updates. Each query requires a `driver` property that specifies which database system the query is for. The migration system automatically filters queries and executes only those matching your current database driver. ```yaml - type: sql mode: execute query: - driver: mysql sql: 'INSERT INTO test_table (test_value) VALUES ("foo");' - driver: sqlite sql: 'INSERT INTO test_table (test_value) VALUES ("foo");' - driver: postgresql sql: "INSERT INTO test_table (test_value) VALUES ('foo');" ``` The supported database drivers are: - `mysql` - MySQL/MariaDB - `postgresql` - PostgreSQL - `sqlite` - SQLite You can define queries for multiple database drivers in a single migration step. The system executes only the queries that match your configured database platform. If no matching queries are found, the migration throws an error. > **Caution: Caution** > > SQL migrations bypass the content model abstraction layer and directly modify the database. Use them with caution and ensure your queries are compatible with your target database system. ### Error handling with try-catch You can wrap one or more migration steps with a `try_catch` step to handle exceptions gracefully. Use it for migration steps that may fail under specific conditions but should not halt the entire migration process. For example, you can ensure a language creation migration step succeeds even if the language already exists. If the migration step fails for this reason, the exception is suppressed, allowing the remaining migrations to proceed without interruption. A `try_catch` migration requires the `steps` property and accepts optional `allowed_exceptions` and `stop_after_first_exception` settings. Default values are: - `allowed_exceptions`: empty list - `stop_after_first_exception`: `true` ```yaml - type: try_catch mode: execute allowed_exceptions: - Ibexa\Contracts\Core\Repository\Exceptions\InvalidArgumentException stop_after_first_exception: true steps: - type: language mode: create metadata: languageCode: ger-DE name: German enabled: true ``` When an exception is thrown within a `try_catch` step, it's compared against the list of `allowed_exceptions`. If the exception matches, it's caught and the migration step continues or stops depending on the `stop_after_first_exception` configuration setting. The migration step is marked as successful and the migration process continues. Non-matching exceptions throw immediately, halting the migration process and returning an error. ### Service calls You can call a method of a service by using the `service_call` migration type. Use it when a migration requires custom logic that isn't covered by the built-in migration types. A `service_call` migration requires the `service` and `method` properties, and accepts an optional `arguments` list passed to the method: ```yaml - type: service_call mode: execute service: App\Migration\ContentImporter method: import arguments: - content.csv - 42 ``` You can only call services that are explicitly listed under the `ibexa_migrations.callable_services` configuration key: ```yaml ibexa_migrations: callable_services: - App\Migration\ContentImporter ``` ### Expression syntax You can use [Symfony expression syntax](https://symfony.com/doc/7.4/reference/formats/expression_language.html) in data migrations, like in [repeatable steps](#repeatable-steps), where you can use it to generate varied content in migration steps. The expression syntax uses the following structure: `### ###` The `IDENTIFIER` can be any repeated string that encloses the actual expression. #### Built-in functions Built-in expression language functions that are tagged with `ibexa.migrations.template.expression_language.function`: - `to_bool`, `to_int`, `to_float`, `to_string` - convert various data types by passing them into PHP casting functions (like `floatval`, `intval`, and others). ```yaml - fieldDefIdentifier: show_children languageCode: eng-US value: '###XXX to_bool(i % 3) XXX###' - fieldDefIdentifier: quantity languageCode: eng-US value: '###XXX to_int("42") XXX###' - fieldDefIdentifier: price languageCode: eng-US value: '###XXX to_float("19.99") XXX###' - fieldDefIdentifier: description languageCode: eng-US value: '###XXX to_string(123) XXX###' ``` - `reference` - references a specific object or resource within your application or configuration. Learn more about [migration references](https://doc.ibexa.co/en/saas/content_management/data_migration/managing_migrations/#references). ```yaml - fieldDefIdentifier: some_field languageCode: eng-US value: '###XXX reference("example_reference") XXX###' ``` - `project_dir` - retrieves the project's root directory path, for example to construct file paths or access project-specific resources. ```yaml - fieldDefIdentifier: project_directory languageCode: eng-US value: '###XXX project_dir() XXX###' ``` - `env` - retrieves the value of an environmental variable. ```yaml - type: user mode: update match: field: login value: admin metadata: email: admin@example.com enabled: true password: '###XXX env("ADMIN_PASSWORD") XXX###' ``` #### Custom functions To add custom functionality into Migration's expression language declare it as a service and tag it with `ibexa.migrations.template.expression_language.function`. Example: ```yaml ibexa.migrations.template.to_bool: class: Closure factory: [ Closure, fromCallable ] arguments: - 'boolval' tags: - name: 'ibexa.migrations.template.expression_language.function' function: to_bool ibexa.migrations.template.faker: class: Closure factory: [ Closure, fromCallable ] arguments: - 'Faker\Factory::create' tags: - name: 'ibexa.migrations.template.expression_language.function' function: faker ``` Service-based functions can be also added, but they must be callable, requiring either an `__invoke` function or a wrapping service with one. ## Migration examples The following examples show what data you can import using data migrations. ### Content types The following example shows how to create a content type with two field definitions. The required metadata keys are: `identifier`, `mainTranslation`, `contentTypeGroups` and `translations`. The example also shows the optional metadata keys: `nameSchema`, `urlAliasSchema`, `container`, `defaultAlwaysAvailable`, `defaultSortField`, `defaultSortOrder`, `remoteId`, and `creatorId`. The default values of field definition properties mirror the underlying PHP API, for example: - `translatable` defaults to `true` - `required` defaults to `false` ```yaml - type: content_type mode: create metadata: identifier: blog_post mainTranslation: eng-GB remoteId: blog_post_content_type creatorId: 14 nameSchema: '' urlAliasSchema: '<title>' container: true defaultAlwaysAvailable: true defaultSortField: 2 # Location::SORT_FIELD_PUBLISHED defaultSortOrder: 0 # Location::SORT_ORDER_DESC contentTypeGroups: - Content translations: eng-GB: name: Blog Post fields: - identifier: title type: ibexa_string required: true translations: eng-GB: name: 'Title' - identifier: body type: ibexa_richtext required: false translations: eng-GB: name: 'Body' ``` ### Content items The following example shows how to create two content items: a folder and an article inside it. When creating a content item, three metadata keys are required: `contentType`, `parentLocationId`, and `mainTranslation`. The `mainTranslation` property sets content item's main language. To use the location ID of the folder, which is created automatically by the system, you can use a [reference](https://doc.ibexa.co/en/saas/content_management/data_migration/managing_migrations/#references). In this case you assign the `parent_folder_location_id` reference name to the location ID, and then use it when creating the article. ```yaml - type: content mode: create metadata: contentType: folder mainTranslation: eng-GB location: parentLocationId: 2 fields: - fieldDefIdentifier: name languageCode: eng-GB value: 'Parent folder' references: - name: parent_folder_location_id type: location_id - type: content mode: create metadata: contentType: article mainTranslation: eng-GB location: parentLocationId: 'reference:parent_folder_location_id' fields: - fieldDefIdentifier: title languageCode: eng-GB value: 'Child article' - fieldDefIdentifier: intro languageCode: eng-GB value: xml: | <?xml version="1.0" encoding="UTF-8"?> <section xmlns="http://docbook.org/ns/docbook" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:ezxhtml="http://ibexa.co/xmlns/dxp/docbook/xhtml" xmlns:ezcustom="http://ibexa.co/xmlns/dxp/docbook/custom" version="5.0-variant ezpublish-1.0"><para>This is <emphasis role="strong">article into</emphasis>.</para></section> ``` The following example shows the optional `metadata` and `location` properties that you can set when creating a content item. Instead of `parentLocationId`, you can identify the parent location with `parentLocationRemoteId`. `sortField` takes the numeric value of one of the `SORT_FIELD_*` constants from the [`Location` class](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-Core-Repository-Values-Content-Location.html), and `sortOrder` takes `ASC` or `DESC`, case insensitive: ```yaml - type: content mode: create metadata: contentType: folder mainTranslation: eng-GB creatorId: 14 modificationDate: '2025-06-01T10:00:00+00:00' publicationDate: '2025-06-01T10:00:00+00:00' remoteId: options_folder alwaysAvailable: true section: identifier: standard location: parentLocationId: 2 # parentLocationRemoteId: root_location locationRemoteId: options_folder_location hidden: false priority: 10 sortField: 9 # Location::SORT_FIELD_NAME sortOrder: ASC fields: - fieldDefIdentifier: name languageCode: eng-GB value: 'Folder with all options' ``` Use the `update` mode to modify an existing content item. You can match the content item by `content_remote_id`, `location_id`, `parent_location_id`, or `content_type_identifier`. All `metadata` keys are optional: [`initialLanguageCode`](https://doc.ibexa.co/en/saas/content_management/content_api/creating_content/#translating-content), [`mainLanguageCode`](https://doc.ibexa.co/en/saas/content_management/content_model/#content-information), `creatorId`, `remoteId`, `alwaysAvailable`, `mainLocationId`, `modificationDate`, `publishedDate`, `name`, and `ownerId`. ```yaml - type: content mode: update match: field: content_remote_id value: options_folder metadata: mainLanguageCode: eng-GB name: 'Updated folder' initialLanguageCode: eng-GB remoteId: f5c88a2209584891056f987fd965b0ba mainLocationId: 5 creatorId: 14 ownerId: 14 publishedDate: '2002-10-06T15:19:56+00:00' alwaysAvailable: false modificationDate: '2025-06-15T10:00:00+00:00' fields: - fieldDefIdentifier: name languageCode: eng-GB value: 'Updated folder' ``` Use the `delete` mode to delete content items: ```yaml - type: content mode: delete match: field: content_remote_id value: __REMOTE_ID__ only_visible_content: true # Optional, default: true. If set to true, only visible content will be deleted. allow_no_delete: false # Optional, default: false. If set to true, the migration will not fail if no content is found using the specified criteria. ``` ### Images The following example shows how to migrate an `example-image.png` located in `public/var/site/storage/images/3/8/3/0/383-1-eng-GB` without manually placing it in the appropriate path. To prevent the manual addition of images to specific DFS or local locations, such as `public/var/site/storage/images/` you can move image files to, for example `src/Migrations/images`. Adjust the migration file and configure the `image` field data as follows: ```yaml - fieldDefIdentifier: image languageCode: eng-GB value: alternativeText: '' fileName: example-image.png path: src/Migrations/images/example-image.png ``` This migration copies the image to the appropriate directory, in this case `public/var/site/storage/images/3/8/3/0/254-1-eng-GB/example-image.png`, enabling swift file migration regardless of storage (local, DFS). ### Roles The following example shows how to create a role. A role requires the `identifier` metadata key. For each policy assigned to the role, you select the module and function, with optional limitations. The following example shows the creation of a `Contributor` role: ```yaml - type: role mode: create metadata: identifier: Contributor policies: - module: content function: read - module: content function: create limitations: - identifier: Class values: [folder, article, blog_post] - identifier: Section values: [standard, media] - module: content function: edit limitations: - identifier: Owner values: ['1'] ``` To update an existing role, two policies' modes are available: - `replace`: (default) All existing policies are replaced by the ones from the migration. - `append`: Migration policies are added while already existing ones are kept. The following example shows how to replace the policies of the existing `Editor` role: ```yaml - type: role mode: update match: field: identifier value: Editor policies: - module: content function: '*' - module: user function: login limitations: - identifier: SiteAccess values: [ admin ] - module: url function: '*' ``` The following example shows the addition of a policy to the `Anonymous` role: ```yaml type: role mode: update match: field: identifier value: Anonymous policies: mode: append list: - module: user function: login limitations: - identifier: SiteAccess values: [ new_siteaccess ] ``` The following example shows how to delete the `Contributor` role: ```yaml - type: role mode: delete match: field: identifier value: Contributor ``` ### Locations The following example shows how to swap content items assigned to given locations. ```yaml - type: location mode: swap match1: field: location_remote_id value: f3e90596361e31d496d4026eb624c983 match2: field: location_id value: 5 ``` The metadata keys for Location are optional. The following example shows how to trash locations. ```yaml - type: location mode: trash match: field: location_remote_id value: f3e90596361e31d496d4026eb624c983 - type: location mode: trash match: field: location_id value: 5 ``` ### Users The following example shows how to create a user. The required metadata keys are: `login`, `email`, `password`, `enabled`, `mainLanguage`, and `contentType`. You also need to provide the user group's remote content ID. You can use an [action](https://doc.ibexa.co/en/saas/content_management/data_migration/data_migration_actions/index.md) to assign a role to the user. ```yaml - type: user mode: create metadata: login: janedoe email: johndoe@example.com password: Password123Password enabled: true mainLanguage: eng-GB contentType: user groups: - 3c160cca19fb135f83bd02d911f04db2 fields: - fieldDefIdentifier: first_name languageCode: eng-GB value: John - fieldDefIdentifier: last_name languageCode: eng-GB value: Doe actions: - { action: assign_user_to_role, identifier: 'Member'} ``` You can also update the user's email, enabled status, and password. All `metadata` keys are optional: ```yaml - type: user mode: update match: field: login value: admin metadata: email: admin@example.com enabled: true password: '###XXX env("ADMIN_PASSWORD") XXX###' ``` ### Languages The following example shows how to create a language. The required metadata keys are: `languageCode`, `name`, and `enabled`. ```yaml - type: language mode: create metadata: languageCode: ger-DE name: German enabled: true ``` You can also update an existing language to rename it or change its enabled state. Both `name` and `enabled` are optional, only the fields you provide are modified. ```yaml - type: language mode: update languageCode: ger-DE metadata: name: 'German (Germany)' enabled: true references: - name: ref__ger_de__language_id type: language_id ``` The example above saves the ID of the updated language as a [reference](https://doc.ibexa.co/en/saas/content_management/data_migration/managing_migrations/#references) for further usage. ### Product catalog #### Attributes and attribute groups The following example shows how to create an attribute group with two attributes: ```yaml - type: attribute_group mode: create identifier: hat names: eng-GB: Hat - type: attribute mode: create identifier: size attribute_type_identifier: integer attribute_group_identifier: hat names: eng-GB: Size - type: attribute mode: create identifier: color attribute_type_identifier: selection attribute_group_identifier: hat names: eng-GB: Color options: choices: - value: red label: "eng-GB": "Red" - value: white label: "eng-GB": "White" - value: black label: "eng-GB": "Black" ``` You can also update attributes, including changing which attribute group they belong to: ```yaml - type: attribute mode: update criteria: type: field_value field: identifier value: width operator: '=' identifier: new_width attribute_group_identifier: size names: eng-GB: New Width ``` You can't change the attribute type of an existing attribute. ##### Date and time attributes You can manage the [date and time attribute type](https://doc.ibexa.co/en/saas/product_catalog/attributes/date_and_time/index.md) through the migrations, for example: ```yaml - type: attribute mode: create identifier: event_date attribute_group_identifier: example attribute_type_identifier: datetime position: 1 names: eng-GB: 'Event date' options: accuracy: day # One of: second, minute, day, month, trimester, year ``` #### Product types The following example shows how to create a product type. The main part of the migration file is the same as when creating a regular content type. A product type must also contain the definition for an `ibexa_product_specification` field. `fieldSettings` contains information about the product attributes. ```yaml - type: content_type mode: create metadata: identifier: hat mainTranslation: eng-GB contentTypeGroups: - product translations: eng-GB: name: Hat fields: - identifier: name type: ibexa_string required: true translations: eng-GB: name: Name - identifier: specification type: ibexa_product_specification required: true translatable: false translations: eng-GB: name: Specification fieldSettings: attributes_definitions: dimensions: - { attributeDefinition: size, required: true, discriminator: false } - { attributeDefinition: color, required: true, discriminator: true } ``` #### Products The following example shows how to create a product: ```yaml - type: content mode: create metadata: contentType: hat mainTranslation: eng-GB location: parentLocationId: 60 fields: - fieldDefIdentifier: name languageCode: eng-GB value: 'Top hat 58cm' - fieldDefIdentifier: specification languageCode: eng-GB value: code: top_hat__58 attributes: size: 58 is_virtual: false ``` #### Product variants The following example shows how to create variants for a product identified by its code: ```yaml - type: product_variant mode: create base_product_code: top_hat__58 variants: - code: top_hat__58__white attributes: color: white - code: top_hat__58__black attributes: color: black ``` #### Product assets The following example creates an image [content item](#content-items) from a local image file, and then uses it as a product asset for a variant ([created in previous example](#product-variants)): ```yaml - type: content mode: create metadata: contentType: image mainTranslation: eng-GB location: parentLocationId: 51 # Media/Images fields: - fieldDefIdentifier: name languageCode: eng-GB value: 'Top hat 58cm Black' - fieldDefIdentifier: image languageCode: eng-GB value: alternativeText: 'Top hat 58cm Black' fileName: 'top_hat_58cm_black.jpg' path: top_hat_58cm_black.jpg references: - name: top_hat_58cm_black_image_content_id type: content_id - type: product_asset mode: create product_code: top_hat__58__black uri: '### "ezcontent://"~reference("top_hat_58cm_black_image_content_id") ###' tags: [] ``` This migration uses a [reference](https://doc.ibexa.co/en/saas/content_management/data_migration/managing_migrations/#references) to store the created image content ID, and then uses it while creating the asset. It uses an [expression syntax](#expression-syntax) to [concatenate (`~`)](https://symfony.com/doc/7.4/reference/formats/expression_language.html#string-operators) the mandatory scheme `ezcontent://` and the image content ID through the [`reference` function](#built-in-functions) used on the reference's name. #### Product prices The following example shows how to create a price for a product identified by its code: ```yaml - type: product_price mode: create product_code: top_hat__58 currency_code: 'EUR' amount: 120 custom_prices: - customer_group: contractors base_amount: 120 custom_amount: 100 ``` #### Product availability The following example shows how to define the availability and stock of a product identified by its code: ```yaml - type: product_availability mode: create product_code: ergo_desk is_available: true is_infinite: false stock: 100 ``` When `is_infinite` is set to `true`, `stock` must be `null`. #### Customer groups The following example shows how to create a customer group with a defined global price discount: ```yaml - type: customer_group mode: create identifier: contractors names: eng-GB: Contractors global_price_rate: -20.0 ``` #### Currencies The following example shows how to create a currency: ```yaml - type: currency mode: create code: TST subunits: 3 enabled: true # default, optional ``` ### Segments (Experience) The following example shows how to create a segment group and add segments in it: ```yaml - type: segment_group mode: create name: 'Contractors' identifier: contractors references: - name: contractors_group_id type: segment_group_id - type: segment mode: create name: 'Painter' identifier: painter group: identifier: contractors ``` When updating a segment group or segment, you can match the object to update by using its numerical ID or identifier: ```yaml - type: segment mode: update name: 'Painter and Finish' matcher: identifier: painter ``` ### Settings The following example shows how you can create and update a setting stored in the database: ```yaml - type: setting mode: create group: test identifier: my_setting value: first: first_value second: second_value - type: setting mode: update group: test identifier: my_setting value: first: first_value_modified ``` ### Taxonomies The following example shows how you can create a "Car" tag in the main Taxonomy: ```yaml - type: content mode: create metadata: contentType: tag mainTranslation: eng-GB alwaysAvailable: true section: identifier: taxonomy location: parentLocationRemoteId: taxonomy_tags_folder fields: - fieldDefIdentifier: name languageCode: eng-GB value: Car - fieldDefIdentifier: identifier languageCode: eng-GB value: car - fieldDefIdentifier: parent languageCode: eng-GB value: taxonomy_entry_identifier: root ``` The field identifiers must match the identifiers used in the `ibexa_taxonomy` configuration file. If the content type associated with the tags is changed, the configuration should be adjusted when creating migrations. > **Note: Note** > > If there are multiple taxonomies, the `taxonomy` field is then necessary here (line 21). You can use the following example to assign tags to a Content (content type Article has an additional field): ```yaml - type: content mode: create metadata: contentType: article mainTranslation: eng-GB alwaysAvailable: false section: identifier: standard location: parentLocationId: 42 fields: - fieldDefIdentifier: title languageCode: eng-GB value: Test1 - fieldDefIdentifier: short_title languageCode: eng-GB value: test1 - fieldDefIdentifier: author languageCode: eng-GB value: - id: '1' name: 'Administrator User' email: admin@link.invalid - fieldDefIdentifier: intro languageCode: eng-GB value: xml: | <?xml version="1.0" encoding="UTF-8"?> <section xmlns="http://docbook.org/ns/docbook" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:ezxhtml="http://ibexa.co/xmlns/dxp/docbook/xhtml" xmlns:ezcustom="http://ibexa.co/xmlns/dxp/docbook/custom" version="5.0-variant ezpublish-1.0"><para>test</para></section> - fieldDefIdentifier: enable_comments languageCode: eng-GB value: false - fieldDefIdentifier: field_631b169ec8035 languageCode: eng-GB value: taxonomy_entries_identifiers: - car - plane taxonomy: tags ``` When updating a content type, use: ```yaml - type: content_type mode: update match: field: content_type_identifier value: article fields: - identifier: field_631b169ec8035 type: ibexa_taxonomy_entry_assignment position: 7 translations: eng-GB: name: tag description: 'Tagi' required: false searchable: true infoCollector: false translatable: true category: content defaultValue: taxonomy_entries: { } taxonomy: null fieldSettings: taxonomy: tags validatorConfiguration: { } ``` ### AI action configurations - The following example shows how you can create a new action configuration in your system: ```yaml - type: action_configuration mode: create identifier: foo_identifier enabled: false names: 'eng-GB': 'foo_name_eng_gb' 'ger-DE': 'foo_name_ger_de' descriptions: 'ger-DE': 'foo_description_ger_de' action_handler_identifier: foo_handler action_handler_options: handler_option: foo action_type_identifier: generate_alt_text action_type_options: max_length: 130 ``` - Use the `update` mode to modify an existing action configuration: ```yaml - type: action_configuration mode: update match: field: identifier value: foo_identifier enabled: false identifier: bar_identifier names: 'eng-GB': 'bar_name_eng_gb' 'ger-DE': 'bar_name_ger_de' descriptions: 'ger-DE': 'bar_description_ger_de' action_handler_options: handler_option: bar action_type_options: max_length: 120 ``` - Use the `delete` mode to delete an existing action configuration: ```yaml - type: action_configuration mode: delete match: field: identifier value: foo_identifier ``` ## Criteria When using `update` or `delete` modes, you can use criteria to identify the objects to operate on. > **Caution: Caution** > > Criteria only work with objects related to the product catalog. ```yaml type: currency mode: update criteria: type: field_value field: code value: EUR operator: '=' # default code: EEE subunits: 3 enabled: false ``` Available operators are: - `=` - `<>` - `<` - `<=` - `>` - `>=` - `IN` - `NIN` - `CONTAINS` - `STARTS_WITH` - `ENDS_WITH` You can combine criteria by using logical criteria `and` and `or`: ```yaml type: or criteria: - type: field_value field: code value: EUR - type: field_value field: code value: X operator: STARTS_WITH ``` Criteria can be nested. # Exporting data > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). Export repository data to use in future data migrations. To see an example of migrations in action, export data already present in your installation. To export repository content, use the `ibexa:migrations:generate` command. This command generates a YAML file with the requested part of the repository. The file is located by default in the `src/Migrations/Ibexa/migrations` folder or in [a custom folder that you configure](https://doc.ibexa.co/en/saas/content_management/data_migration/managing_migrations/#migration-folders). You can later use this file to import the data. ```bash php bin/console ibexa:migrations:generate --type=content --mode=create --siteaccess=admin ``` This generates a file containing all content items. Below you can see part of the output of the default Cohesivo installation. ```yaml - type: content mode: create metadata: contentType: user_group mainTranslation: eng-GB creatorId: 14 modificationDate: '2002-10-06T17:19:56+02:00' publicationDate: '2002-10-06T17:19:56+02:00' remoteId: f5c88a2209584891056f987fd965b0ba alwaysAvailable: true section: id: 2 identifier: users location: parentLocationId: 1 parentLocationRemoteId: null locationRemoteId: 3f6d92f8044aed134f32153517850f5a hidden: false sortField: 1 sortOrder: 1 priority: 0 fields: - fieldDefIdentifier: name languageCode: eng-GB value: Users - fieldDefIdentifier: description languageCode: eng-GB value: 'Main group' references: - name: ref__content__user_group__users type: content_id - name: ref__location__user_group__users type: location_id - name: ref__path__user_group__users type: path ``` The output contains all the possible information for a future migration command. Parts of it can be removed or modified. You can treat it as a template for another content item for user group. For example, you could: - Remove `references` if you don't intend to store IDs for future use (see [migration references](https://doc.ibexa.co/en/saas/content_management/data_migration/managing_migrations/#references)) - Remove `publicationDate`, `modificationDate`, `locationRemoteId`, as those are generated if not passed (like in PHP API) - Add [`actions`](https://doc.ibexa.co/en/saas/content_management/data_migration/data_migration_actions/index.md) - Add fields for other languages present in the system. Similarly, you can create update and delete operations. They're particularly functional combined with `match-property`. This option is automatically added as part of `match` expression in the update/delete migration: ```bash php bin/console ibexa:migrations:generate --type=content_type --mode=update --match-property=content_type_identifier --value=article ``` ```yaml - type: content_type mode: update match: field: content_type_identifier value: article metadata: identifier: article mainTranslation: eng-GB modifierId: 14 modificationDate: '2012-07-24T14:35:34+00:00' remoteId: c15b600eb9198b1924063b5a68758232 urlAliasSchema: '' nameSchema: '<short_title|title>' container: true defaultAlwaysAvailable: false defaultSortField: 1 defaultSortOrder: 1 translations: eng-GB: name: Article fields: - identifier: title type: ibexa_string position: 1 translations: eng-GB: name: Title required: true searchable: true infoCollector: false translatable: true category: '' defaultValue: 'New article' fieldSettings: { } validatorConfiguration: StringLengthValidator: maxStringLength: 255 minStringLength: null # - ... ``` You should test your migrations. See [Importing data](https://doc.ibexa.co/en/saas/content_management/data_migration/importing_data/index.md). > **Tip: Tip** > > Migration command can be executed with database rollback at the end with the `--dry-run` option. > **Caution: Caution** > > [`--siteaccess` option](#siteaccess) usage can be relevant when multiple languages or multiple repositories are used. To prevent translation loss, it's recommended that you use the SiteAccess that has all the languages used in your implementation, most likely the back office one. ## type The mandatory `--type` option defines the type of repository data to export. The following types are available: - `content` - `content_type` - `role` - `content_type_group` - `user` - `user_group` - `language` - `object_state_group` - `object_state` - `section` - `location` - `attribute_group` - `attribute` - `segment` - `segment_group` - `company` If you don't provide the `--type` option, the command asks you to select a type of data. ## mode The mandatory `--mode` option defines the action that importing the file performs. The following modes are available: - `create` - creates new items. - `update` - updates an existing item. Only covers specified fields and properties. If the item doesn't exist, causes an error. - `delete` - deletes an existing item. If the item doesn't exist, causes an error. If you don't provide the `--mode` option, the command asks you to select the mode. The following combinations of types are modes are available: | | `create` | `update` | `delete` | | -------------------- | -------- | -------- | -------- | | `content` | Yes | Yes | Yes | | `content_type` | Yes | Yes | | | `role` | Yes | Yes | Yes | | `content_type_group` | Yes | Yes | | | `user` | Yes | Yes | | | `user_group` | Yes | Yes | Yes | | `language` | Yes | Yes | | | `object_state_group` | Yes | | | | `object_state` | Yes | | | | `section` | Yes | Yes | | | `location` | | Yes | | | `attribute_group` | Yes | Yes | Yes | | `attribute` | Yes | Yes | Yes | | `segment` | Yes | Yes | Yes | | `segment_group` | Yes | Yes | Yes | | `company` | Yes | | | ## siteaccess The optional `--siteaccess` option enables you to export (or import) data in a SiteAccess configuration's context. If not provided, the [default SiteAccess](https://doc.ibexa.co/en/saas/multisite/multisite_configuration/#default-siteaccess) is used. It's recommended that you use the SiteAccess of the target repository's back office. Specifying the SiteAccess can be mandatory, for example, when you use several SiteAccesses to handle [several languages](https://doc.ibexa.co/en/saas/multisite/languages/languages/#using-siteaccesses-for-handling-translations). Export and import commands only work with languages supported by the context SiteAccess. You must export and import with the SiteAccess supporting all the languages to preserve translations. This option is also important if you use [several repositories with their own databases](https://doc.ibexa.co/en/saas/administration/configuration/repository_configuration/#defining-custom-connection). ## match-property The optional `--match-property` option, together with `value`, enables you to select which data from the repository to export. `match-property` defines what property should be used as a criterion for selecting data. The following properties are available (per type): - `content` - `content_id` - `content_type_id` - `content_type_group_id` - `content_type_identifier` - `content_remote_id` - `location_id` - `location_remote_id` - `parent_location_id` - `user_id` - `user_email` - `user_login` - `content_type` - `content_type_identifier` - `content_type_group` - `content_type_group_id` - `content_type_group_identifier` - `language` - `language_code` - `location` - `location_remote_id` - `location_id` - `object_state` - `object_state_id` - `object_state_identifier` - `object_state_group` - `object_state_group_id` - `object_state_group_identifier` - `role` - `identifier` - `id` - `section` - `section_id` - `section_identifier` - `user` - `login` - `email` - `id` - `user_group` - `id` - `remoteId` - `attribute` - `id` - `identifier` - `type` - `attribute_group_id` - `position` - `options` - `attribute_group` - `identifier` You can extend the list of available matchers by creating [a custom one](https://doc.ibexa.co/en/saas/content_management/data_migration/add_data_migration_matcher/index.md). ## value The optional `--value` option, together with `match-property`, filters the repository content that the command exports. `value` defines which values of the `match-property` should be included in the export. For example, to export only Article content items, use the `content_type_identifier` match property with `article` as the value: ```bash php bin/console ibexa:migrations:generate --type=content --mode=create --match-property=content_type_identifier --value=article ``` > **Note: Note** > > The same `match-property` and `value` is added to generated `update` and `delete` type migration files. ## file The optional `--file` option defines the name of the YAML file to export to. ```bash php bin/console ibexa:migrations:generate --type=content --mode=create --file=my_data_export.yaml ``` > **Note: Note** > > When migrating multiple files at once (for example when calling `ibexa:migrations:migrate` without options), they're executed in alphabetical order. ## user-context The optional `--user-context` option enables you to run the export command as a specified user. The command only exports repository data that the selected user has access to. By default the admin account is used, unless specifically overridden by this option or in bundle configuration (`ibexa_migrations.default_user_login`). ```bash php bin/console ibexa:migrations:generate --type=content --mode=create --user-context=jessica_andaya ``` # Managing migrations > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). Manage data migrations by adding files, converting from Kaliop migration bundle, checking migration status, and setting up configuration. ## Converting migration files If you want to convert a file from the format used by the [Kaliop migration bundle](https://github.com/kaliop-uk/ezmigrationbundle) to the current migration format, use the `ibexa:migrations:kaliop:convert` command. The source file must use Kaliop mode and type combinations. The converter handles Kaliop types that are different from Ibexa types. ```bash php bin/console ibexa:migrations:kaliop:convert --input=kaliop_format.yaml --output=ibexa_format.yaml ``` You can also convert multiple files using `ibexa:migrations:kaliop:bulk-convert`: ```bash php bin/console ibexa:migrations:kaliop:bulk-convert --recursive kaliop_files ibexa_files ``` If you don't specify the output folder, the command overwrites the input files. ## Adding migration files Use the `ibexa:migrations:import` command to add files to the migration folder defined in configuration (by default, `src/Migrations/Ibexa/migrations`). ```bash php bin/console ibexa:migrations:import my_data_export.yaml ``` ## Checking migration status To check the status of migration files in the migration folder defined in configuration, run the following command: ```bash php bin/console ibexa:migrations:status ``` The command lists the migration files and indicates which of them have already been migrated. ## Migration folders The default migration folder is `src/Migrations/Ibexa/migrations`. You can configure a different folder by using the following settings: ```yaml ibexa_migrations: migration_directory: '%kernel.project_dir%/src/Migrations/MyMigrations/' migrations_files_subdir: migration_files ``` > **Note: Multi-repository scenario** > > In multi-repository environments, where data for different websites is stored in separate databases, you can migrate such databases separately, to prevent the migration processes from affecting each other. > > The `ibexa_migrations.migration_directory` setting accepts a placeholder within a path. The placeholder is dynamically replaced by a name of the repository that you want to migrate, based on the selected SiteAccess. > > ```yaml > ibexa_migrations: > migration_directory: '%kernel.project_dir%/data/<repository>' > ``` > > Then, when you run the migration command, you must use the [`--siteaccess` option](https://doc.ibexa.co/en/saas/content_management/data_migration/exporting_data/#siteaccess) and provide the name of the SiteAccess that you want to migrate. ## Preview configuration You can get default configuration along with option descriptions by executing the following command: ```bash bin/console config:dump-reference ibexa_migrations ``` ## References References are key-value pairs necessary when one migration depends on another. Since some migrations generate object properties (like IDs) during their execution, which cannot be known in advance, references provide migrations with the ability to use previously created object properties in further migrations. They can be subsequently used by passing them in their desired place with `reference:` prefix. The example below creates the content item of type "folder" named "Media" below the root, and stores its location path as `"ref__path__folder__media"` to use it later while creating a related role. Then this reference is reused as part of a new role, as a limitation. ```yaml - type: content mode: create metadata: contentType: folder mainTranslation: eng-US alwaysAvailable: true section: 3 objectStates: { } location: parentLocationId: 1 hidden: false sortField: !php/const Ibexa\Contracts\Core\Repository\Values\Content\Location::SORT_FIELD_NAME sortOrder: 1 priority: 0 fields: - fieldDefIdentifier: name languageCode: eng-US value: Media # - ... actions: { } references: - name: ref__content__folder__media type: content_id - name: ref__location__folder__media type: location_id - name: ref__path__folder__media type: path - type: role mode: create metadata: identifier: foo policies: - module: content function: 'read' limitations: - identifier: Subtree values: ['reference:ref__path__folder__media'] ``` By default, references are stored in memory and can be reused within the same migration file without additional steps. To reuse them across different migration files, you can save them to disk. Reference files are located in a separate directory `src/Migrations/Ibexa/references` (for more information, see [previewing reference](#preview-configuration) `ibexa_migrations.migration_directory` and `ibexa_migrations.references_files_subdir` options). When saving references, existing files with the same name are overwritten. Reference files **aren't** loaded by default. A separate step (`type: reference`, `mode: load`, with `filename` with a relative path as value) is required. Similarly, saving a reference file is done using `type: reference`, `mode: save` step, with filename. References must be **loaded before** they can be used in the same migration file. The order of migration steps matters - they are executed sequentially from top to bottom. ```yaml - type: reference mode: load filename: 'references/references.yaml' # Load references created by other migrations # Use them - type: content mode: create # ... # Save any new references if needed - type: reference mode: save filename: 'references/new_references.yaml' ``` You can also set a reference value manually with the `set` mode, and use the `list` mode to print all references collected so far to the migration log: ```yaml - type: reference mode: set name: parent_location_id value: 2 - type: reference mode: list ``` ## Available reference types - `content` - `content_id` - `location_id` - `path` - `content_type` - `content_type_id` - `language` - `language_id` - `language_code` - `role` - `role_id` - `section` - `section_id` - `user` - `user_id` - `user_group` - `user_group_id` # Data migration actions > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). Data migration actions enable you to run special operations while executing data migrations, such as assigning roles, sections, Objects states, and more. Some migration steps can contain a special `actions` property. You can find which migration steps support actions in the table below: | | `create` | `update` | `delete` | | -------------- | -------- | -------- | -------- | | `content` | Yes | Yes | Yes | | `content_type` | Yes | Yes | Yes | | `role` | Yes | Yes | | | `user` | Yes | Yes | | | `user_group` | Yes | Yes | | | `company` | Yes | | | Actions are optional operations that can be run after the main "body" of a migration has been executed (for example, content has been created / updated, object state has been added). Their purpose is to allow additional operations to be performed as part of this particular migration. They're executed inside the same transaction, so in the event of failure they cause database rollback to occur. For example, when updating a content type object, some fields might be removed: ```yaml - type: content_type mode: update match: field: content_type_identifier value: article actions: - { action: assign_content_type_group, value: 'Media' } - { action: unassign_content_type_group, value: 'Content' } - { action: remove_field_by_identifier, value: 'short_title' } - { action: remove_drafts, value: null } ``` When executed, this migration: - Finds content type using its identifier (`article`) - Assigns content type group "Media" - Removes it from content type group "Content" - Removes the `short_title` field - Removes its existing drafts, if any. ## Available migration actions The following migration actions are available out of the box: - `assign_dashboard_to_user` (Content Create) - `assign_object_state` (Content Create) - `assign_parent_location` (Content Create / Update) - `assign_section` (Content Update) - `hide` (Content Create / Update) - `reveal` (Content Create / Update) - `assign_content_type_group` (Content type Create / Update) - `remove_drafts` (Content type Update) - `remove_field_by_identifier` (Content type Update) - `unassign_content_type_group` (Content type Update) - `add_block_to_available_blocks` (Content type Update) - `assign_role_to_user` (Role Create / Update) - `assign_role_to_user_group` (Role Create / Update) - `assign_user_to_role` (User Create / Update) - `assign_user_group_to_role` (User group Create / Update) - `unassign_role_user_group` (User group Update) In contrast with Kaliop migrations, actions provide you with ability to perform additional operations and extend the migration functionality. For more information, see [creating your own Actions](https://doc.ibexa.co/en/saas/content_management/data_migration/create_data_migration_action/index.md). ## Action usage examples ### Content mode: Create ```yaml actions: - { action: assign_object_state, identifier: locked, groupIdentifier: ibexa_lock } - { action: assign_parent_location, value: 2 } - { action: hide } ``` mode: Update ```yaml actions: - { action: assign_parent_location, value: 2 } - { action: assign_section, id: 4 } - { action: assign_section, identifier: 'media' } ``` When creating a [dashboard](https://doc.ibexa.co/en/saas/administration/dashboard/customize_dashboard/index.md) content item, you can assign it to a specific user, identified by their login: ```yaml actions: - { action: assign_dashboard_to_user, value: admin } ``` ### Content types mode: Create ```yaml actions: - { action: assign_content_type_group, value: 'Media' } ``` mode: Update ```yaml actions: - { action: assign_content_type_group, value: 'Media' } - { action: unassign_content_type_group, value: 'Content' } - { action: remove_field_by_identifier, value: 'short_title' } - { action: remove_drafts, value: null } - { action: add_block_to_available_blocks, fieldDefinitionIdentifier: 'page', blocks: ['event'] } ``` ### Roles mode: Create and Update ```yaml actions: - action: assign_role_to_user_group remote_id: 'remote_id_152454854' - action: assign_role_to_user_group id: 42 - action: assign_role_to_user id: 42 - action: assign_role_to_user email: 'mail@invalid.c' - action: assign_role_to_user login: foo ``` ### Users mode: Create and Update ```yaml actions: - action: assign_user_to_role identifier: foo - action: assign_user_to_role id: 2 - action: assign_user_to_role id: 2 limitation: type: Section values: - 1 ``` ### User groups mode: Create and Update ```yaml actions: - action: assign_user_group_to_role identifier: Editor - action: assign_user_group_to_role id: 2 - action: assign_user_group_to_role id: 1 limitation: type: Section values: - 1 ``` > **Note: Note** > > In the `assign_user_group_to_role` action, limitation type section can only use section ID. mode: Update ```yaml actions: - action: unassign_role_user_group id: 1 ``` > **Note: Note** > > In the `unassign_role_user_group` action, the ID is role assignment ID from the `ibexa_user_role` table. # Create data migration step > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). Create a custom step for data migrations. Besides the [built-in migrations steps](https://doc.ibexa.co/en/saas/content_management/data_migration/importing_data/#available-migrations), you can also create custom ones. To create a custom migration step, you need: - A step class, to store any additional data that you might require. - A step normalizer, to convert YAML definition into your step class. - A step executor, to handle the step. The following example shows how to create a step that replaces all `ibexa_string` fields that have an old company name with "New Company Name". ## Create step class First, create a step class, in `src/Migrations/Step/ReplaceNameStep.php`: ```php <?php declare(strict_types=1); namespace App\Migrations\Step; use Ibexa\Migration\ValueObject\Step\StepInterface; final readonly class ReplaceNameStep implements StepInterface { private string $replacement; public function __construct(?string $replacement = null) { $this->replacement = $replacement ?? 'New Company Name'; } public function getReplacement(): string { return $this->replacement; } } ``` ## Create normalizer Then you need a normalizer to convert data that comes from YAML into a step object, in `src/Migrations/Step/ReplaceNameStepNormalizer.php`: ```php <?php declare(strict_types=1); namespace App\Migrations\Step; use Ibexa\Contracts\Migration\Serializer\AbstractStepNormalizer; use Ibexa\Migration\ValueObject\Step\StepInterface; /** * @extends \Ibexa\Contracts\Migration\Serializer\AbstractStepNormalizer<\App\Migrations\Step\ReplaceNameStep> */ final class ReplaceNameStepNormalizer extends AbstractStepNormalizer { protected function normalizeStep( StepInterface $object, ?string $format = null, array $context = [] ): array { assert($object instanceof ReplaceNameStep); return [ 'replacement' => $object->getReplacement(), ]; } protected function denormalizeStep( $data, string $type, string $format, array $context = [] ): ReplaceNameStep { return new ReplaceNameStep($data['replacement'] ?? null); } public function getHandledClassType(): string { return ReplaceNameStep::class; } public function getType(): string { return 'company_name'; } public function getMode(): string { return 'replace'; } } ``` Then, tag the step normalizer, so it's recognized by the serializer used for migrations. ```yaml App\Migrations\Step\ReplaceNameStepNormalizer: tags: - 'ibexa.migrations.serializer.step_normalizer' - 'ibexa.migrations.serializer.normalizer' ``` ## Create executor And finally, create an executor to perform the step, in `src/Migrations/Step/ReplaceNameExecutor.php`: ```php <?php declare(strict_types=1); namespace App\Migrations\Step; use Ibexa\Contracts\Core\Repository\ContentService; use Ibexa\Contracts\Core\Repository\Values\Filter\Filter; use Ibexa\Contracts\Migration\StepExecutor\AbstractStepExecutor; use Ibexa\Core\FieldType\TextLine\Value; use Ibexa\Migration\ValueObject\Step\StepInterface; final class ReplaceNameStepExecutor extends AbstractStepExecutor { public function __construct(private readonly ContentService $contentService) { } protected function doHandle(StepInterface $step) { assert($step instanceof ReplaceNameStep); $contentItems = $this->contentService->find(new Filter()); foreach ($contentItems as $contentItem) { $struct = $this->contentService->newContentUpdateStruct(); foreach ($contentItem->getFields() as $field) { if ($field->fieldTypeIdentifier !== 'ibexa_string') { continue; } if ($field->fieldDefIdentifier === 'identifier') { continue; } if (str_contains((string) $field->value, 'Company Name')) { $newValue = str_replace('Company Name', $step->getReplacement(), $field->value); $struct->setField($field->fieldDefIdentifier, new Value($newValue)); } } try { $content = $this->contentService->createContentDraft($contentItem->contentInfo); $content = $this->contentService->updateContent($content->getVersionInfo(), $struct); $this->contentService->publishVersion($content->getVersionInfo()); } catch (\Throwable) { // Ignore } } return null; } public function canHandle(StepInterface $step): bool { return $step instanceof ReplaceNameStep; } } ``` Tag the executor with `ibexa.migrations.step_executor` tag. ```yaml App\Migrations\Step\ReplaceNameStepExecutor: tags: - 'ibexa.migrations.step_executor' ``` Then you can create a migration file that represents this step in your application: ```yaml - type: company_name mode: replace replacement: 'New Company Name' # as declared in normalizer, this is optional ``` # Create data migration action > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). Create a custom action to use while performing data migration. To create an [action](https://doc.ibexa.co/en/saas/content_management/data_migration/data_migration_actions/index.md) that is performed after a migration step, you need: - An action class, to store any additional data that you might require. - An action denormalizer, to convert YAML definition into your action class. - An action executor, to handle the action. The following example shows how to create an action that assigns a content item to a Section. First, create an action class, in `src/Migrations/Action/AssignSection.php`: ```php <?php declare(strict_types=1); namespace App\Migrations\Action; use Ibexa\Migration\ValueObject\Step\Action; final readonly class AssignSection implements Action { public const string TYPE = 'assign_section'; public function __construct(private string $sectionIdentifier) { } /** * @return string */ public function getValue(): string { return $this->sectionIdentifier; } public function getSupportedType(): string { return self::TYPE; } } ``` Then you need a denormalizer to convert data that comes from YAML into an action object, in `src/Migrations/Action/AssignSectionDenormalizer.php`: ```php <?php declare(strict_types=1); namespace App\Migrations\Action; use Ibexa\Contracts\Migration\Serializer\Denormalizer\AbstractActionDenormalizer; use Webmozart\Assert\Assert; final class AssignSectionDenormalizer extends AbstractActionDenormalizer { protected function supportsActionName(string $actionName, ?string $format = null): bool { return $actionName === AssignSection::TYPE; } /** * @param array<mixed> $data * @param string $type * @param string|null $format * @param array<mixed> $context * * @return \App\Migrations\Action\AssignSection */ public function denormalize($data, string $type, ?string $format = null, array $context = []): AssignSection { Assert::keyExists($data, 'value'); return new AssignSection($data['value']); } } ``` Then, tag the action denormalizer so it's recognized by the serializer used for migrations. ```yaml services: App\Migrations\Action\AssignSectionDenormalizer: autoconfigure: false tags: - { name: 'ibexa.migrations.serializer.normalizer' } ``` And finally, add an executor to perform the action, in `src/Migrations/Action/AssignSectionExecutor.php`: ```php <?php declare(strict_types=1); namespace App\Migrations\Action; use Ibexa\Contracts\Core\Repository\ContentService; use Ibexa\Contracts\Core\Repository\SectionService; use Ibexa\Contracts\Core\Repository\Values\ValueObject as APIValueObject; use Ibexa\Migration\StepExecutor\ActionExecutor\ExecutorInterface; use Ibexa\Migration\ValueObject; final readonly class AssignSectionExecutor implements ExecutorInterface { public function __construct( private ContentService $contentService, private SectionService $sectionService ) { } /** * @param \App\Migrations\Action\AssignSection $action * @param \Ibexa\Contracts\Core\Repository\Values\Content\Content $content */ public function handle(ValueObject\Step\Action $action, APIValueObject $content): void { $contentInfo = $this->contentService->loadContentInfo($content->id); $section = $this->sectionService->loadSectionByIdentifier($action->getValue()); $this->sectionService->assignSection($contentInfo, $section); } } ``` Tag the executor with `ibexa.migrations.executor.action.<type>` tag, where `<type>` is the "type" of the step that executor works with (for example, `content`, `content_type`, or `location`). The tag has to have a `key` property with the action type. ```yaml App\Migrations\Action\AssignSectionExecutor: tags: - { name: 'ibexa.migrations.executor.action.content', key: !php/const App\Migrations\Action\AssignSection::TYPE } ``` # Create data migration matcher > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). Create a matcher for handling data migrations. [Matchers in data migrations](https://doc.ibexa.co/en/saas/content_management/data_migration/exporting_data/#match-property) enable you to select which data from the repository to export. In addition to the built-in matchers, you can create custom matchers for content. The following example creates a matcher for section identifiers. ## Create normalizer To do this, first add a normalizer which handles the conversion between objects and the YAML format used for data migration. Matchers are instances of `FilteringCriterion`, so a custom normalizer needs to denormalize into an instance of `FilteringCriterion`. > **Tip: Normalizers** > > To learn more about normalizers, refer to [Symfony documentation](https://symfony.com/doc/7.4/serializer.html). Create the normalizer in `src/Migrations/Matcher/SectionIdentifierNormalizer.php`: ```php <?php declare(strict_types=1); namespace App\Migrations\Matcher; use Ibexa\Bundle\Migration\Serializer\Normalizer\Criterion\AbstractCriterionNormalizer; use Ibexa\Contracts\Core\Repository\Values\Content\Query\Criterion; use Ibexa\Contracts\Core\Repository\Values\Filter\FilteringCriterion; use Webmozart\Assert\Assert; class SectionIdentifierNormalizer extends AbstractCriterionNormalizer { public function __construct() { parent::__construct('section_identifier'); } /** * @param array<mixed> $data * @param array<mixed> $context */ protected function createCriterion(array $data, string $type, ?string $format, array $context): FilteringCriterion { Assert::keyExists($data, 'value'); return new Criterion\SectionIdentifier($data['value']); } public function supportsNormalization($data, ?string $format = null, array $context = []): bool { return $data instanceof Criterion\SectionIdentifier; } } ``` Register the normalizer as a service: ```yaml App\Migrations\Matcher\SectionIdentifierNormalizer: tags: - { name: 'ibexa.migrations.serializer.normalizer' } ``` > **Note: Normalizer order** > > User-defined normalizers are always executed before the built-in ones. However, you can additionally set the priority of your normalizers. > > Check the priorities of all normalization services by using: > > ```bash > php bin/console debug:container --tag ibexa.migrations.serializer.normalizer > ``` ## Create generator Additionally, if you want to export data using the `ibexa:migrations:generate` command, you need a generator. Create the generator in `src/Migrations/Matcher/SectionIdentifierGenerator.php`: ```php <?php declare(strict_types=1); namespace App\Migrations\Matcher; use Ibexa\Contracts\Core\Repository\Values\Content\Query\Criterion; use Ibexa\Migration\Generator\CriterionGenerator\GeneratorInterface; final class SectionIdentifierGenerator implements GeneratorInterface { public static function getMatchProperty(): string { return 'section_identifier'; } public function generate($value): Criterion { return new Criterion\SectionIdentifier($value); } } ``` Register the generator as a service: ```yaml App\Migrations\Matcher\SectionIdentifierGenerator: tags: - { name: 'ibexa.migrations.generator.criterion_generator.content' } ``` # Data migration API > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). You can use the PHP API to run data migrations, add new migration files, or get information about available migrations. You can use the PHP API to manage and run [data migrations](https://doc.ibexa.co/en/saas/content_management/data_migration/data_migration/index.md). ## Getting migration information To list all migration files available in the directory defined in configuration (by default, `src/Migrations/Ibexa`), use the `MigrationService:listMigrations()` method: ```php foreach ($this->migrationService->listMigrations() as $migration) { $output->writeln($migration->getName()); } ``` To get a single migration file by its name, use the `MigrationService:findOneByName()` method: ```php $my_migration = $this->migrationService->findOneByName($migration_name); ``` ## Running migration files To run migration file(s), use either `MigrationService:executeOne()` or `MigrationService:executeAll()`: ```php $this->migrationService->executeOne($my_migration); $this->migrationService->executeAll('admin'); ``` Both `executeOne()` and `executeAll()` can take an optional parameter: the login of the User that you want to execute the migrations as. ## Adding new migrations To add a new migration file, use the `MigrationService:add()` method: ```php $this->migrationService->add( new Migration( 'new_migration.yaml', $string_with_migration_content ) ); ``` # Field types > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). Field types define the fields that a content item is built of. Field types are the smallest building blocks of content. Cohesivo comes with many [built-in field types](https://doc.ibexa.co/en/saas/content_management/field_types/field_type_reference/field_type_reference/#available-field-types) that cover most common needs, for example, Text line, Email address, Author list, Content relation, Map location, or Float. Field types are responsible for: - Storing data, either using the native storage engine mechanisms or specific means - Validating input data - Making the data searchable (if applicable) - Displaying fields of this type ## Custom data Cohesivo can support custom data to be stored in the fields of a content item. To do so, you need to create a custom field type. A custom field type must implement the **FieldType Service Provider Interfaces** available in the [`Ibexa\Core\FieldType`](https://github.com/ibexa/core/tree/6.0/src/lib/FieldType) namespace. > **Note: Registration** > > Remember that all your custom field types must be registered in `config/services.yml`. For more information, see [Registration](https://doc.ibexa.co/en/saas/content_management/field_types/type_and_value/#registration). To provide custom functionality for a field type, the SPI interacts with multiple layers of the Cohesivo architecture: ![Field type Overview](https://doc.ibexa.co/en/saas/content_management/img/field_type_overview.png) On the top layer, the field type needs to provide conversion from and to a simple PHP hash value to support the **REST API**. The generated hash value may only consist of scalar values and hashes. It must not contain objects or arrays with numerical indexes that aren't sequential and/or don't start with zero. > **Caution: Simple hash values** > > A simple hash value always means an array of scalar values and/or nested arrays of scalar values. To avoid issues with format conversion, don't use objects inside the simple hash values. Below that, the field type must support the **public PHP API** implementation regarding: - Settings definition for `FieldDefinition` - Value creation and validation - Communication with the Persistence SPI On the bottom level, a field type can additionally hook into the **Persistence SPI** to store data from a `FieldValue` in an external service. All non-standard Cohesivo database tables (for example, `ibexa_url`) are treated as [external storage](https://doc.ibexa.co/en/saas/content_management/field_types/field_type_storage/#storing-data-externally). The following sequence diagrams visualize the process of creating and publishing new content across all layers, especially focused on the interaction with a field type. ## Creating content ![Create content sequence](https://doc.ibexa.co/en/saas/content_management/img/create_content_sequence.png) ## Publishing content > **Note: indexLocation()** > > For **Solr** locations are indexed during Content indexing. For **Legacy/SQL** indexing isn't required as location data already exists in a database. ![Publish content sequence](https://doc.ibexa.co/en/saas/content_management/img/publish_content_sequence.png) ## Updating content ![Update content sequence](https://doc.ibexa.co/en/saas/content_management/img/update_content_sequence.png) ## Loading content ![Load content sequence](https://doc.ibexa.co/en/saas/content_management/img/load_content_sequence.png) # Type and Value > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). The basis of all field types are their Type and Value classes, containing, respectively, the logic and the data for the fields. A field type must contain a Type class which contains the logic of the field type, for example, validating data, transforming from various formats, or describing the validators. A Type class must implement `Ibexa\Core\FieldType\FieldType` ("field type interface"). All native field types also extend the `Ibexa\Core\FieldType\FieldType` abstract class that implements this interface and provides implementation facilities through a set of abstract methods of its own. You should also provide a value object class for storing the custom field value provided by the field type. The Value is used to represent an instance of the field type within a content item. Each field presents its data using an instance of the Type's Value class. A Value class must implement the `Ibexa\Contracts\Core\FieldType` interface. It may also extend the `Ibexa\Core\FieldType\Value` abstract class. It's meant to be stateless and as lightweight as possible. This class must contain as little logic as possible, because the logic is handled by the Type class. ## Type class The Type class of a field type provides an implementation of the [`Ibexa\Contracts\Core\FieldType\FieldType`](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-Core-FieldType-FieldType.html) interface. ### Field Definition handling A custom field type is used in a field definition of a custom content type. You can additionally provide [settings for the field type](#field-type-settings) and a [validator configuration](https://doc.ibexa.co/en/saas/content_management/field_types/field_type_validation/index.md). Since the public PHP API cannot know anything about these, their handling is delegated to the field type itself through the following methods: #### `getFieldTypeIdentifier()` Returns a unique identifier for the custom field type which is used to assign the type to a field definition. By convention it should be prefixed by a unique vendor shortcut (for example, `ibexa` for Cohesivo). #### `getSettingsSchema()` This method retrieves via public PHP API a schema for the field type settings. A typical setting would be, for example, default value. The settings structure defined by this schema is stored in the `FieldDefinition`. Since it's not possible to define a generic format for such a schema, the field type is free to return any serializable data structure from this method. #### `getValidatorConfigurationSchema()` In addition to normal settings, the field type should provide schema settings for its validation process. The schema describes what kind of validation can be performed by the field type and which settings the user can specify to these validation methods. For example, the `ibexa_string` type can validate minimum and maximum length of the string. It therefore provides a schema to indicate to the user that they might specify the corresponding restrictions, when creating a `FieldDefinition` with this type. The schema doesn't underlie any regulations, except for that it must be serializable. #### `validateFieldSettings()` The type is asked to validate the settings (provided by the user) before the public PHP API stores those settings for the field type in a `FieldDefinition`. As a result, the field type must return if the given settings comply to the schema defined by `getSettingsSchema()`. #### `validateValidatorConfiguration()` As in `validateFieldSettings()`, this method verifies that the given validator configuration complies to the schema provided by `getValidatorConfigurationSchema()`. It's important to know that the schema definitions of the field type can be both of arbitrary and serializable format. It's highly recommended to use a simple hash structure. > **Note: Note** > > Since it's not possible to enforce a schema format, the code using a specific field type must basically know all field types it deals with. This also applies to all user interfaces and the REST API, which therefore must provide extension points to register handling code for custom field type. These extensions aren't defined yet. ### Field type name The content item name is retrieved by the `Ibexa\Core\FieldType\FieldType::getName` method which must be implemented. To generate content item name or URL alias the field type name must be a part of a name schema or a URL schema. ## Value handling A field type needs to deal with the custom value format provided by it. In order for the public PHP API to work properly, it delegates working with such custom field values to the corresponding field type. The `Ibexa\Core\FieldType\FieldType` interface therefore provides the following methods: ### `acceptValue()` This method is responsible for accepting and converting user input for the field. It checks the input structure by accepting, building, and returning a different structure holding the data. For example: a user provides an HTTP link as a string, `acceptValue()` converts the link to a URL field type value object. Unlike the `FieldType\Value` constructor, it's possible to make this method aware of multiple input types (object or primitive). > **Note: Note** > > `acceptValue()` asserts structural consistency of the value, but doesn't validate plausibility of the value. ### `getEmptyValue()` The field type can specify that the user may define a default value for the `Field` of the type through settings. If no default value is provided, the field type is asked for an "empty value" as the final fallback. The value chain for filling a specific field of the field type is as follows: 1. Is a value provided by the filling user? 2. If not, is a default value provided by the`FieldDefinition`? 3. If not, take the empty value provided by the `FieldType`. ### `validate()` In contrast to `acceptValue()` this method validates the plausibility of the given value. It's based on the field type settings and validator configuration and stored in the corresponding `FieldDefinition`. ## Serialization When [REST API](https://doc.ibexa.co/en/saas/api/rest_api/rest_api_usage/rest_api_usage/index.md) is used, conversion needs to be done for field type values, settings, and validator configurations. These are converted to and from a simple hash format that can be encoded in REST payload. As conversion needs to be done both when transmitting and receiving data through REST, field type implements the following pairs of methods: | Method | Description | | ---------------------------------- | -------------------------------------------------------------------- | | `toHash()` | Converts field type Value into a simple hash format. | | `fromHash()` | Converts the other way around. | | `fieldSettingsToHash()` | Converts field type settings to a simple hash format. | | `fieldSettingsFromHash()` | Converts the other way around. | | `validatorConfigurationToHash()` | Converts field type validator configuration to a simple hash format. | | `validatorConfigurationFromHash()` | Converts the other way around. | > **Caution: Simple hash values** > > A simple hash value always means an array of scalar values and/or nested arrays of scalar values. To avoid issues with format conversion, don't use objects inside the simple hash values. ## Registration The field type must be registered in `config/services.yml`: ```yaml services: Ibexa\FieldTypeMatrix\FieldType\Type: parent: Ibexa\Core\FieldType\FieldType tags: - {name: ibexa.field_type, alias: ibexa_matrix} ``` ### `parent` As described in the [Symfony service container documentation](https://symfony.com/doc/7.4/service_container/advanced_definitions.html#parent-services), the `parent` config key indicates that you want your service to inherit from the parent's dependencies, including constructor arguments and method calls. This helps to avoid repetition in your field type configuration and keeps consistency between all field types. If you need to inject other services into your Type class, skip using the `parent` config key. ### `tags` Like most API components, field types use the [Symfony service tag mechanism](https://symfony.com/doc/7.4/service_container/tags.html). A service can be assigned one or several tags, with specific parameters. When the [service container](https://doc.ibexa.co/en/saas/api/php_api/php_api/#service-container) is compiled into a PHP file, tags are read by `CompilerPass` implementations that add extra handling for tagged services. Each service tagged as `ibexa.field_type` is added to a [registry](https://martinfowler.com/eaaCatalog/registry.html) using the `alias` key as its unique `fieldTypeIdentifier`, for example, `ibexa_string`. Each field type must also inherit from the abstract `ibexa.field_type` service. This ensures that the initialization steps shared by all field types are executed. > **Tip: Tip** > > The configuration of built-in field types is located in [`core/src/lib/Resources/settings/fieldtypes.yml`](https://github.com/ibexa/core/blob/6.0/src/lib/Resources/settings/fieldtypes.yml). ### Indexing To make the search engine aware of the data stored in a field type, register it as [indexable](https://doc.ibexa.co/en/saas/content_management/field_types/field_type_search/index.md) ## Field type settings It's recommended to use a simple associative array format for the settings schema returned by `Ibexa\Contracts\Core\FieldType\FieldType::getSettingsSchema()`, which follows these rules: - The key of the associative array identifies a setting (for example, `default`) - Its value is an associative array describing the setting using: - `type` to identify the setting type (for example, `int` or `string`) - `default` containing the default setting value An example schema could look like this: ```php [ 'backupData' => [ 'type' => 'bool', 'default' => false, ], 'defaultValue' => [ 'type' => 'string', 'default' => 'Default Value', ], ]; ``` The settings are mapped into Symfony forms via the [FormMapper](https://doc.ibexa.co/en/saas/content_management/field_types/form_and_template/#formmapper). > **Note: Note** > > You can store field type settings internally, or, when the schema becomes too complex, move them to [external storage](https://doc.ibexa.co/en/saas/content_management/field_types/field_type_storage/#storing-field-type-settings-externally). ## Extensibility points Some field types require additional processing, for example a field type storing a binary file, or one having more complex settings, or validator configuration. For this purpose specific implementations of an abstract class `Ibexa\Contracts\Rest\FieldTypeProcessor` are used. This class provides the following methods: | Method | Description | | ----------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `preProcessValueHash()` | Performs manipulations on a received value hash, so that it conforms to the format expected by the `fromHash()` method described above. | | `postProcessValueHash()` | Performs manipulations on a outgoing value hash, previously generated by the `toHash()` method described above. | | `preProcessFieldSettingsHash()` | Performs manipulations on a received settings hash, so that it conforms to the format expected by the `fieldSettingsFromHash()` method described above. | | `postProcessFieldSettingsHash()` | Performs manipulations on a outgoing settings hash, previously generated by the `fieldSettingsToHash()` method described above. | | `preProcessValidatorConfigurationHash()` | Performs manipulations on a received validator configuration hash, so that it conforms to the format expected by the `validatorConfigurationFromHash()` method described above. | | `postProcessValidatorConfigurationHash()` | Performs manipulations on a outgoing validator configuration hash, previously generated by the `validatorConfigurationToHash()` method described above. | Base implementations of these methods return the given hash, so you can implement only the methods your field type requires. Some built-in field types already implement processors and you're encouraged to take a look at them. # Form and template > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). Field type FormMappers allow field editing, while custom templates ensure the field can be rendered both in the back office and on site front. ## FormMapper The FormMapper maps field definitions into Symfony forms, allowing field editing. It can implement two interfaces: - `Ibexa\Contracts\ContentForms\FieldType\FieldValueFormMapperInterface` to provide editing support - `Ibexa\AdminUi\FieldType\FieldDefinitionFormMapperInterface` to provide field type definition editing support, when you require non-standard settings ### FieldValueFormMapperInterface The `FieldValueFormMapperInterface::mapFieldValueForm` method accepts two arguments: - `FormInterface` — form for the current field - `FieldData` — underlying data for current field form You have to add your form type to the content editing form. The example shows how `ibexa_boolean` injects the form: ```php use Ibexa\ContentForms\Form\Type\FieldType\CheckboxFieldType; use Ibexa\Contracts\ContentForms\Data\Content\FieldData; use Ibexa\Contracts\ContentForms\FieldType\FieldValueFormMapperInterface; use Symfony\Component\Form\FormInterface; class MyMapper implements FieldValueFormMapperInterface { /** @param FormInterface<mixed> $fieldForm */ public function mapFieldValueForm(FormInterface $fieldForm, FieldData $data): void { $fieldDefinition = $data->getFieldDefinition(); $formConfig = $fieldForm->getConfig(); $fieldForm ->add( $formConfig->getFormFactory()->createBuilder() ->create( 'value', CheckboxFieldType::class, [ 'required' => $fieldDefinition->isRequired, 'label' => $fieldDefinition->getName( $formConfig->getOption('languageCode') ), ] ) ->setAutoInitialize(false) ->getForm() ); } } ``` Your type has to be called `value`. In the example above, `CheckboxFieldType::class` is used, but you can use standard Symfony form type instead. It's good practice to encapsulate fields with custom types as it allows easier templating. Type has to be compatible with your field type's `Ibexa\Core\FieldType` implementation. You can use a [`DataTransformer`](https://symfony.com/doc/7.4/form/data_transformers.html) to achieve that or assure correct property and form field names. ### FieldDefinitionFormMapperInterface Providing definition editing support is almost identical to creating content editing support. The only difference are field names: ```php use Ibexa\AdminUi\FieldType\FieldDefinitionFormMapperInterface; use Ibexa\AdminUi\Form\Data\FieldDefinitionData; use Ibexa\ContentForms\Form\Type\FieldType\CountryFieldType; use Symfony\Component\Form\Extension\Core\Type\CheckboxType; use Symfony\Component\Form\FormInterface; class MyMapper implements FieldDefinitionFormMapperInterface { /** @param FormInterface<mixed> $fieldDefinitionForm */ public function mapFieldDefinitionForm(FormInterface $fieldDefinitionForm, FieldDefinitionData $data): void { $fieldDefinitionForm ->add( 'isMultiple', CheckboxType::class, [ 'required' => false, 'property_path' => 'fieldSettings[isMultiple]', 'label' => 'field_definition.ibexa_country.is_multiple', ] ) ->add( $fieldDefinitionForm->getConfig()->getFormFactory()->createBuilder() ->create( 'defaultValue', CountryFieldType::class, [ 'choices_as_values' => true, 'multiple' => true, 'expanded' => false, 'required' => false, 'label' => 'field_definition.ibexa_country.default_value', ] ) // Deactivate auto-initialize as you're not on the root form. ->setAutoInitialize(false)->getForm() ); } } ``` Use names corresponding to the keys used in field type's `Ibexa\Core\FieldType\FieldType::$settingsSchema` implementation. The special `defaultValue` key allows you to specify a field for setting the default value assigned during content editing. ### Registering the service The FormMapper must be registered as a service: ```yaml App\FieldType\Mapper\CustomFieldTypeMapper: tags: - { name: ibexa.admin_ui.field_type.form.mapper.definition, fieldType: custom } - { name: ibexa.admin_ui.field_type.form.mapper.value, fieldType: custom } ``` Tag the mapper according to the support you need to provide: - Add the `ibexa.admin_ui.field_type.form.mapper.value` tag when providing content editing support (`FieldValueFormMapperInterface` interface). - Add the `ibexa.admin_ui.field_type.form.mapper.definition` tag when providing field type definition editing support (`FieldDefinitionFormMapperInterface` interface). The `fieldType` key has to correspond to the name of your field type. ## Content view templates To render the field in content view by using the `ibexa_render_field()` Twig helper, you need to define a template containing a block for the field. ```html+twig {% block customfieldtype_field %} {# Your code here #} {% endblock %} ``` By convention, your block must be named `<fieldTypeIdentifier>_field`. > **Tip: Tip** > > Template blocks for built-in field types are available in [`Core/Resources/views/content_fields.html.twig`](https://github.com/ibexa/core/blob/6.0/src/bundle/Core/Resources/views/content_fields.html.twig). > > This template is also exposed as a part of Standard Design, so you can override it with the [design engine](https://doc.ibexa.co/en/saas/templating/design_engine/design_engine/index.md). To do so, place the template `themes/standard/content_fields.html.twig` in your `Resources/views` (assuming `ibexa_standard_design.override_kernel_templates` is set to true). ### Template variables The block can receive the following variables: | Name | Type | Description | | --------------- | ------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `field` | `Ibexa\Contracts\Core\Repository\Values\Content\Field` | The field to display | | `contentInfo` | `Ibexa\Contracts\Core\Repository\Values\Content\ContentInfo` | The ContentInfo of the content item the field belongs to | | `versionInfo` | `Ibexa\Contracts\Core\Repository\Values\Content\VersionInfo` | The VersionInfo of the content item the field belongs to | | `fieldSettings` | array | Settings of the field (depends on the field type) | | `parameters` | hash | Options passed to `ibexa_render_field()` under the `'parameters'` key | | `attr` | hash | The attributes to add the generate the HTML markup, passed to `ibexa_render_field()` under the `'attr'` key. Contains at least a class entry, containing `<fieldtypeidentifier>-field` | ### Reusing blocks For easier field type template development you can take advantage of all defined blocks by using the [`block()` function](https://twig.symfony.com/doc/3.x/functions/block.html). You can for example use `simple_block_field`, `simple_inline_field` or `field_attributes` blocks provided in [`content_fields.html.twig`](https://github.com/ibexa/core/blob/6.0/src/bundle/Core/Resources/views/content_fields.html.twig#L486). > **Caution: Caution** > > To be able to reuse built-in blocks, your template must inherit from `@IbexaCore/content_fields.html.twig`. ### Registering a template If you don't use the [design engine](https://doc.ibexa.co/en/saas/templating/design_engine/design_engine/index.md) or you want to have separate templates per field type and/or SiteAccess, you can register a template under the `ibexa.system.<scope>.field_templates` [configuration key](https://doc.ibexa.co/en/saas/administration/configuration/configuration/#configuration-files): ```yaml ibexa: system: <scope>: field_templates: - template: 'fields/custom_field_template.html.twig' # Priority is optional (default is 0). The higher it is, the higher your template gets in the list. priority: 10 ``` ## Back office templates ### Back office view template For templates for previewing the field in the back office, using the design engine is recommended with `ibexa_standard_design.override_kernel_templates` set to `true`. With the design engine you can apply a template (for example, `Resources/views/themes/admin/content_fields.html.twig`) without any extra configuration. If you don't use the design engine, apply the following configuration: ```yaml ibexa: system: admin_group: field_templates: - { template: 'adminui/field/custom_field_view.html.twig', priority: 10 } ``` ### Field edit template To use a template for the field edit form in the back office, you need to specify it in configuration under the `twig.form_themes` [configuration key](https://doc.ibexa.co/en/saas/administration/configuration/configuration/#configuration-files): ```yaml twig: form_themes: - 'adminui/field/custom_field_template.html.twig' ``` We encourage using custom form types for encapsulation as this makes templating easier by providing Twig block name. All built-in field types are implemented with this approach. In that case overriding form theme can be done with: ```html+twig {% block custom_fieldtype_widget %} Hello world! {{ block('form_widget') }} {% endblock %} ``` For more information on creating and overriding form type templates, see [Symfony documentation](https://symfony.com/doc/7.4/form/create_custom_field_type.html#creating-the-form-type-template). # Field type storage > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). To be able to store the data saved to a field, you must configure storage conversion for the field type. ## Storage conversion If you want to store field values in regular Cohesivo database tables, the `FieldValue` must be converted to the storage-specific format used by the Persistence SPI: `Ibexa\Contracts\Core\Persistence\Content\FieldValue`. After restoring a field of the field type, you must reverse the conversion. The following methods of the field type are responsible for that: | Method | Description | | ------------------------ | ----------------------------------------------------------------------------------------------------------------- | | `toPersistenceValue()` | This method receives the value of a field of the field type and returns an SPI `FieldValue`, which can be stored. | | `fromPersistenceValue()` | This method receives an SPI `FieldValue` and reconstructs the original value of the field from it. | The SPI `FieldValue` struct has properties which the field type can use: | Property | Description | | --------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `$data` | The data to be stored in the database. This may be a scalar value, an associative array or a simple, serializable object. | | `$externalData` | The arbitrary data stored in this field isn't touched by any of the Cohesivo components directly, but is available for [Storing data externally](#storing-data-externally). | | `$sortKey` | A value which can be used to sort content by this field. | ### Legacy storage engine The Legacy storage engine uses the `ibexa_content_field` table to store field values, and `ibexa_content_type_field_definition` to store field definition values. They're both based on the same principle. Each row represents a field or a field definition, and offers several free fields of different types, where the type can store its data. - `ibexa_content_field` offers: - `data_int` - `data_text` - `data_float` - `ibexa_content_type_field_definition` offers: - four `data_int` (`data_int1` to `data_int4`) fields - four `data_float` (`data_float1` to `data_float4`) ones - five `data_text` (`data_text1` to `data_text5`) Each type is free to use those fields in any way it requires. The default Legacy storage engine cannot store arbitrary value information as provided by a field type. This means that using this storage engine requires a conversion. Converters map a field's semantic values to the fields described above, for both settings (validation and configuration) and value. The conversion takes place through the `Ibexa\Core\Persistence\Legacy\Content\FieldValue\Converter` interface, which you must implement in your field type. The interface contains the following methods: | Method | Description | | ---------------------------- | ------------------------------------------------------------------------------------------------- | | `toStorageValue()` | Converts a Persistence `Value` into a Legacy storage specific value. | | `toFieldValue()` | Converts the other way around. | | `toStorageFieldDefinition()` | Converts a Persistence `FieldDefinition` to a storage specific one. | | `toFieldDefinition` | Converts the other way around. | | `getIndexColumn()` | Returns the storage column which is used for indexing either `sort_key_string` or `sort_key_int`. | Just like a Type, a Legacy Converter needs to be registered and tagged in the [service container](https://doc.ibexa.co/en/saas/api/php_api/php_api/#service-container). #### Registering a converter The registration of a `Converter` currently works through the `$config` parameter of [`Ibexa\Core\Persistence\Legacy\Handler`](https://github.com/ibexa/core/blob/6.0/src/lib/Persistence/Legacy/Handler.php). Those converters also need to be correctly exposed as services and tagged with `ibexa.field_type.storage.legacy.converter`: ```yaml services: Ibexa\Core\Persistence\Legacy\Content\FieldValue\Converter\TextLine: tags: - {name: ibexa.field_type.storage.legacy.converter, alias: ibexa_string} ``` The tag has the following attribute: | Attribute name | Usage | | -------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `alias` | Represents the `fieldTypeIdentifier` (like for the [field type service](https://doc.ibexa.co/en/saas/content_management/field_types/type_and_value/#registration)). | > **Tip: Tip** > > Converter configuration for built-in field types is located in [`ibexa/core/src/lib/Resources/settings/fieldtype_external_storages.yml`](https://github.com/ibexa/core/blob/6.0/src/lib/Resources/settings/fieldtype_external_storages.yml). ## Storing data externally A field type may store arbitrary data in external data sources. External storage can be, for example, a web service, a file in the file system, another database or even the Cohesivo database itself (in form of a non-standard table). To store data in external storage, the field type interacts with the Persistence SPI through the `Ibexa\Contracts\Core\FieldType\FieldStorage` interface. Accessing the internal storage of a content item that includes a field of the field type calls one of the following methods to also access the external data: | Method | Description | | ------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `hasFieldData()` | Returns whether the field type stores external data at all. | | `storeFieldData()` | Called right before a field of the field type is stored. The method stores `$externalData`. It returns `true` if the call manipulated internal data of the given field, so that it's updated in the internal database. | | `getFieldData()` | Called after a field has been restored from the database to restore `$externalData`. | | `deleteFieldData()` | Must delete external data for the given field, if exists. | | `getIndexData()` | Returns the actual index data for the provided `Ibexa\Contracts\Core\Persistence\Content\Field`. For more information, see [search service](https://doc.ibexa.co/en/saas/content_management/field_types/field_type_search/#search-field-values). | Each of the above methods (except `hasFieldData`) receives a `$context` array with information on the underlying storage and the environment. To retrieve and store data in the Cohesivo data storage, but outside of the normal structures (for example, a custom table in an SQL database), use [Gateway-based storage](#gateway-based-storage) with properly injected Doctrine Connection. The field type must take care on its own for being compliant with different data sources and that third parties can extend the data source support. ### Gateway-based storage To allow the usage of a field type that uses external data with different data storages, it's recommended to implement a gateway infrastructure and a registry for the gateways. To make this easier, the Core implementation of field types provides corresponding interfaces and base classes. They can also be used for custom field types. The interface `Ibexa\Contracts\Core\FieldType\StorageGateway` is implemented by gateways, to be handled correctly by the registry. It has one method: | Method | Description | | ----------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `setConnection()` | The registry mechanism uses this method to set the SPI storage connection (for example, the database connection to the Legacy Storage database) into the gateway, which might be used to store external data. The connection is retrieved from the `$context` array automatically by the registry. | The Gateway implementation itself must take care of validating that it received a usable connection. If it doesn't, it should throw a `RuntimeException`. The registry mechanism is realized as a base class for `FieldStorage` implementations: `Ibexa\Core\FieldType\GatewayBasedStorage`. For managing `StorageGateway`s, the following methods are already implemented in the base class: | Method | Description | | -------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `addGateway()` | Allows the registration of additional `StorageGateway`s from the outside. Furthermore, an associative array of `StorageGateway`s can be given to the constructor for basic initialization. This array should originate from the dependency injection mechanism. | | `getGateway()` | This protected method is used by the implementation to retrieve the correct `StorageGateway` for the current context. | > **Tip: Tip** > > Refer to the built-in Keyword, URL and User field types for usages of such infrastructure. ### Registering external storage To use external storage, you need to define a service implementing the `Ibexa\Contracts\Core\FieldType\FieldStorage` interface and tag it as `ibexa.field_type.storage.external.handler` to be recognized by the repository. Here is an example for the `myfield` field type: ```yaml services: _defaults: autowire: true autoconfigure: true public: false App\FieldType\MyField\Storage\MyFieldStorage: tags: - {name: ibexa.field_type.storage.external.handler, alias: myfield} ``` The configuration requires providing the `ibexa.field_type.storage.external.handler` tag, with the `alias` attribute being the *fieldTypeIdentifier*. You also have to inject the gateway in `arguments`, [see Gateway-based storage](#gateway-based-storage). External storage configuration for basic field types is located in [`ibexa/core/src/lib/Resources/settings/fieldtype_external_storages.yml`](https://github.com/ibexa/core/blob/6.0/src/lib/Resources/settings/fieldtype_external_storages.yml). Using gateway-based storage requires another service implementing `Ibexa\Core\FieldType\StorageGateway` to be injected into the [external storage handler](#storing-data-externally)). ```yaml services: _defaults: autowire: true autoconfigure: true public: false App\FieldType\MyField\Storage\Gateway\DoctrineStorage: ~ ``` The `ibexa.api.storage_engine.legacy.connection` is of type `Doctrine\DBAL\Connection`. If your gateway still uses an implementation of `eZ\Publish\Core\Persistence\Database\DatabaseHandler` (`eZ\Publish\Core\Persistence\Doctrine\ConnectionHandler`), instead of the `ibexa.api.storage_engine.legacy.connection`, you can pass the `ibexa.api.storage_engine.legacy.dbhandler` service. Also there can be several gateways per field type (one per storage engine). In this case it's recommended to either create base implementation which each gateway can inherit or create interface which each gateway must implement and reference it instead of specific implementation when type-hinting method arguments. > **Tip: Tip** > > Gateway configuration for built-in field types is located in [`core/src/lib/Resources/settings/storage_engines/`](https://github.com/ibexa/core/tree/6.0/src/lib/Resources/settings/storage_engines). ## Storing field type settings externally Just like in the case of data, storing [field type settings](https://doc.ibexa.co/en/saas/content_management/field_types/type_and_value/#field-type-settings) in content item tables may prove insufficient. It's not a problem if your setting specifies, for example, the allowed number of characters in a text field. However, the field type may represent a more complex object, for example, it may consist of two or more other fields, such as the name, product code (SKU), and price, and there can be a set of default values instead of just one. Once you add validation rules for these field values, then it becomes an issue. You can overcome this obstacle: When you create a new field type, you can move field type settings to external storage. > **Note: Note** > > Another benefit of an external storage is that there can be database relations to other objects/entities, and the database itself can maintain the integrity of data. First, create a class that implements the `Ibexa\Contracts\Core\FieldType\FieldConstraintsStorage` interface. Then, register the External Storage as a service and tag it with `ibexa.field_type.external_constraints_storage`. Make sure that the alias you use matches the identifier of the new field type: ```yaml services: App\FieldType\Example\ExternalStorage: tags: - { name: ibexa.field_type.external_constraints_storage, alias: <field_type_identifier> } ``` # Field type validation > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). Field type validation allows you to validate if data entered stored in the field conforms to the schema. ## Validator schema The schema for validator configuration should have a similar format to the [settings schema](https://doc.ibexa.co/en/saas/content_management/field_types/type_and_value/#field-type-settings), except it has an additional level, to group settings for a certain validation mechanism: - The key on the 1st level is a string, identifying a validator - Assigned to that is an associative array (2nd level) of settings - This associative array has a string key for each setting of the validator - It's assigned to a 3rd level associative array, the setting description - This associative array should have the same format as for normal settings For example, for the `ibexa_string` type, the validator schema could be: ```php [ 'stringLength' => [ 'minStringLength' => [ 'type' => 'int', 'default' => 0, ], 'maxStringLength' => [ 'type' => 'int', 'default' => null, ], ], ]; ``` # Field type searching > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). To be searchable, a field type must implement the Indexable interface. Fields, or a custom field type, might contain or maintain data relevant for user searches. To make the search engine aware of the data in your field type you need to implement an additional interface and register the implementation. ## `Indexable` interface The `Ibexa\Contracts\Core\FieldType\Indexable` interface defines the methods below which are required if the field type provides data relevant to search engines. ### `getIndexData(Field $field, FieldDefinition $fieldDefinition)` This method returns the actual index data for the provided `Ibexa\Contracts\Core\Persistence\Content\Field`. The index data consists of an array of `Ibexa\Contracts\Core\Search\Field` instances. They're described below in further detail. ### `getIndexDefinition()` To be able to query data properly an indexable field type also is required to return search specification. You must return an associative array of `Ibexa\Contracts\Core\Search\FieldType` instances from this method, which could look like: ```php use Ibexa\Contracts\Core\Search; return [ 'url' => new Search\FieldType\StringField(), 'text' => new Search\FieldType\StringField(), ]; ``` This example from the `Url` field type shows that the field type always returns two indexable values, both strings. They have the names `url` and `text` respectively. ### `getDefaultMatchField()` This method retrieves the name of the default field to be used for matching. As field types can index multiple fields (see [MapLocation](https://doc.ibexa.co/en/saas/content_management/field_types/field_type_reference/maplocationfield/index.md) field type's implementation of this interface), this method is used to define the default field for matching. Default field is typically used by the [`Field` Search Criterion](https://doc.ibexa.co/en/saas/search/criteria_reference/field_criterion/index.md). ### `getDefaultSortField()` This method gets name of the default field to be used for sorting. As field types can index multiple fields (see [MapLocation](https://doc.ibexa.co/en/saas/content_management/field_types/field_type_reference/maplocationfield/index.md) field type's implementation of this interface), this method is used to define default field for sorting. Default field is typically used by the [`Field` Sort Clause](https://doc.ibexa.co/en/saas/search/sort_clause_reference/field_sort_clause/index.md). ## Register `Indexable` implementations Implement `Ibexa\Contracts\Core\FieldType\Indexable` as an extra service and register this Service using the `ibexa.field_type.indexable` tag. Example from [`indexable_fieldtypes.yaml`](https://github.com/ibexa/core/blob/6.0/src/lib/Resources/settings/indexable_fieldtypes.yml): ```yaml Ibexa\Core\FieldType\Keyword\SearchField: class: Ibexa\Core\FieldType\Keyword\SearchField tags: - {name: ibexa.field_type.indexable, alias: ibexa_keyword} ``` The `alias` should be the same as field type ID. ## Search field values The search field values returned by the `getIndexData` method are simple value objects consisting of the following properties: | Property | Description | | -------- | -------------------------------------------------------------------------------------------------- | | `$name` | The name of the field | | `$value` | The value of the field | | `$type` | An `Ibexa\Contracts\Core\Search\FieldType` instance, describing the type information of the field. | ## Search field types There are many available search field types which are handled by search backend configuration. When using them, there is no need to adapt, for example, the Solr configuration in any way. You can always use custom field types, but these might require re-configuration of the search backend. For Solr this would mean adapting the `schema.xml` file. The default available search field types that can be found in the `Ibexa\Contracts\Core\Search\FieldType` namespace are: | Field type | Description | | ------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `BooleanField` | Boolean values. | | `CustomField` | Custom field, for custom search data types. Probably requires additional configuration in the search backend. | | `DateField` | Date field. Can be used for date range queries. | | `DocumentField` | Document field | | `FloatField` | Field for floating point numbers. | | `FullTextField` | Represents full text searchable value of the field which can be indexed by the legacy search engine. Some full text fields are stored as an array of strings. | | `GeoLocationField` | Field used for Geo location. | | `IdentifierField` | Field used for IDs. Basically acts like the string field, but it's not queried by full-text searches | | `IntegerField` | Field for integer numbers. | | `MultipleBooleanField` | Multiple boolean values. | | `MultipleIdentifierField` | Multiple IDs values. | | `MultipleIntegerField` | Multiple integer numbers. | | `MultipleStringField` | Multiple string values. | | `PriceField` | Field for price values. Currency conversion might be applied by the search backends. Might require careful configuration. | | `StringField` | Standard string values. It's also queried by full text searches. | | `TextField` | Standard text values. It's queried by full text searches. Configured text normalizations in the search backend apply. | ## Configuring Solr As mentioned before, if you use the standard type definitions, there is no need to configure the search backend in any way. The field definitions are handled using `dynamicField` definitions in Solr, for example. If you want to configure the handling of your field, you can always add a special field definition to the Solr `schema.xml`. For fields, the field type names used by the Solr search backend look like this: `<content_type_identifier>/<field_identifier>/<search_field_name>_<type>`. You can define custom `dynamicField` definitions to match, for example, on your custom `_<type>` definition. You could also define a custom field definition for certain fields, like for the name field in an article: ```xml <field name="article/name/value_s" type="string" indexed="true" stored="true" required="false"/> ``` > **Note: Note** > > If you want to learn more about the Solr implementation and detailed information about configuring it, check out the [Solr Search Bundle](https://doc.ibexa.co/en/saas/search/search_engines/solr_search_engine/solr_overview/index.md). # Create custom generic field type > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). Create a new field type based on the Generic field type. The Generic field type is an abstract implementation of field types holding structured data for example, address. You can use it as a base for custom field types. The Generic field type comes with the implementation of basic methods, reduces the number of classes which must be created, and simplifies the tagging process. A more in-depth, step-by-step tutorial can be viewed here: [Creating a Point 2D field type](https://doc.ibexa.co/en/saas/tutorials/generic_field_type/creating_a_point2d_field_type/index.md). > **Tip: Tip** > > You should not use the Generic field type when you need a very specific implementation or complete control over the way data is stored. > **Caution: Simple hash values** > > A simple hash value always means an array of scalar values and/or nested arrays of scalar values. To avoid issues with format conversion, don't use objects inside the simple hash values. ## Define value object First, create `Value.php` in the `src/FieldType/HelloWorld` directory. The Value class of a field type contains only the basic logic of a field type, the rest of it's handled by the `Type` class. For more information about field type Value, see [Value handling](https://doc.ibexa.co/en/saas/content_management/field_types/type_and_value/#value-handling). The `HelloWorld` Value class should contain: - public properties that retrieve `name` - an implementation of the `__toString()` method ```php <?php declare(strict_types=1); namespace App\FieldType\HelloWorld; use Ibexa\Contracts\Core\FieldType\Value as ValueInterface; use Symfony\Component\Validator\Constraints as Assert; final class Value implements ValueInterface { /** * @Assert\NotBlank() */ private ?string $name = null; public function getName(): ?string { return $this->name; } public function setName(?string $name): void { $this->name = $name; } public function __toString(): string { return "Hello {$this->name}!"; } } ``` ## Define fields and configuration Next, implement a definition of a field type extending the Generic field type in the `src/FieldType/HelloWorld/Type.php` class. It provides settings for the field type and an implementation of the `Ibexa\Contracts\Core\FieldType\FieldType` abstract class. ```php <?php declare(strict_types=1); namespace App\FieldType\HelloWorld; use Ibexa\Contracts\ContentForms\FieldType\FieldValueFormMapperInterface; use Ibexa\Contracts\Core\FieldType\Generic\Type as GenericType; final class Type extends GenericType implements FieldValueFormMapperInterface { public function getFieldTypeIdentifier(): string { return 'hello_world'; } } ``` For more information about the Type class of a field type, see [Type class](https://doc.ibexa.co/en/saas/content_management/field_types/type_and_value/#type-class). Next, register the field type as a service and tag it with `ibexa.field_type`: ```yaml services: App\FieldType\HelloWorld\Type: public: true tags: - { name: ibexa.field_type, alias: hello_world } ``` ## Define form for value object Create a `src/Form/Type/HelloWorldType.php` form. It enables you to edit the new field type. ```php <?php declare(strict_types=1); namespace App\Form\Type; use App\FieldType\HelloWorld\Value; use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\Extension\Core\Type\TextType; use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\OptionsResolver\OptionsResolver; final class HelloWorldType extends AbstractType { public function buildForm(FormBuilderInterface $builder, array $options): void { $builder->add('name', TextType::class); } public function configureOptions(OptionsResolver $resolver): void { $resolver->setDefaults([ 'data_class' => Value::class, ]); } } ``` Now you can map field definitions into Symfony forms with FormMapper. Add the `mapFieldValueForm()` method required by `FieldValueFormMapperInterface` and the required `use` statements to `src/FieldType/HelloWorld/Type.php`: ```php <?php declare(strict_types=1); namespace App\FieldType\HelloWorld; use App\Form\Type\HelloWorldType; use Ibexa\Contracts\ContentForms\Data\Content\FieldData; use Ibexa\Contracts\ContentForms\FieldType\FieldValueFormMapperInterface; use Ibexa\Contracts\Core\FieldType\Generic\Type as GenericType; use Symfony\Component\Form\FormInterface; final class Type extends GenericType implements FieldValueFormMapperInterface { public function getFieldTypeIdentifier(): string { return 'hello_world'; } public function mapFieldValueForm(FormInterface $fieldForm, FieldData $data): void { $definition = $data->getFieldDefinition(); $fieldForm->add('value', HelloWorldType::class, [ 'required' => $definition->isRequired, 'label' => $definition->getName(), ]); } } ``` For more information about the FormMappers, see [field type form and template](https://doc.ibexa.co/en/saas/content_management/field_types/form_and_template/index.md). Next, add the `ibexa.admin_ui.field_type.form.mapper.value` tag to the service definition: ```yaml services: App\FieldType\HelloWorld\Type: public: true tags: - { name: ibexa.field_type, alias: hello_world } - { name: ibexa.admin_ui.field_type.form.mapper.value, fieldType: hello_world } ``` ## Render fields ### Create a template Create a template for the new field type. It defines the default rendering of the `HelloWorld` field. In the `templates/themes/standard/field_types` directory create a `field_type.html.twig` file: ```html+twig {% block hello_world_field %} Hello <b>{{ field.value.getName() }}!</b> {% endblock %} ``` ### Template mapping Provide the template mapping under the `ibexa.system.<scope>.field_templates` [configuration key](https://doc.ibexa.co/en/saas/administration/configuration/configuration/#configuration-files): ```yaml ibexa: system: default: field_templates: - { template: '@ibexadesign/field_types/field_type.html.twig', priority: 0 } ``` ## Final results Finally, you should be able to add a new content type in the back office interface. Navigate to **Content types** tab and under **Content** category create a new content type: ![Creating new content type](https://doc.ibexa.co/en/saas/content_management/img/extending_field_type_create.png) Next, define a **Hello World** field: ![Defining Hello World](https://doc.ibexa.co/en/saas/content_management/img/extending_field_type_definition.png) After saving, your **Hello World** content type should be available under **Content** in the sidebar menu. ![Creating Hello World](https://doc.ibexa.co/en/saas/content_management/img/extending_field_type_hello_world.png) # Create custom field type comparison > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). Enable comparison of content fields based on a custom field type. In the back office, you can compare the contents of fields. Comparing is possible only between two versions of the same field that are in the same language. You can add the possibility to compare custom and other unsupported field types. > **Note: Note** > > The following task uses the [custom "Hello World" field type](https://doc.ibexa.co/en/saas/content_management/field_types/create_custom_generic_field_type/index.md). The configuration is based on the comparison mechanism created for the `ibexa_string` field type. ## Create Comparable class First, create a `Comparable.php` class in `src/FieldType/HelloWorld/Comparison`. This class implements the `Ibexa\Contracts\VersionComparison\FieldType\Comparable` interface with the `getDataToCompare()` method: ```php <?php declare(strict_types=1); namespace App\FieldType\HelloWorld\Comparison; use Ibexa\Contracts\Core\FieldType\Value as SPIValue; use Ibexa\Contracts\VersionComparison\FieldType\Comparable as ComparableInterface; use Ibexa\Contracts\VersionComparison\FieldType\FieldTypeComparisonValue; use Ibexa\VersionComparison\ComparisonValue\StringComparisonValue; final class Comparable implements ComparableInterface { public function getDataToCompare(SPIValue $value): FieldTypeComparisonValue { return new Value([ 'name' => new StringComparisonValue([ 'value' => $value->getName(), ]), ]); } } ``` The `getDataToCompare()` fetches the data to compare and determines which [comparison engines](#create-comparison-engine) should be used. Register this class as a service: ```yaml services: App\FieldType\HelloWorld\Comparison\Comparable: tags: - { name: ibexa.field_type.comparable, alias: hello_world } ``` ## Create comparison value Next, create a `src/FieldType/HelloWorld/Comparison/Value.php` file that holds the comparison value: ```php <?php declare(strict_types=1); namespace App\FieldType\HelloWorld\Comparison; use Ibexa\Contracts\VersionComparison\FieldType\FieldTypeComparisonValue; class Value extends FieldTypeComparisonValue { /** @var \Ibexa\VersionComparison\ComparisonValue\StringComparisonValue */ public \Ibexa\VersionComparison\ComparisonValue\StringComparisonValue $name; } ``` ## Create comparison engine The comparison engine handles the operations required for comparing the contents of fields. Each field type requires a separate comparison engine, which implements the `Ibexa\Contracts\VersionComparison\Engine\FieldTypeComparisonEngine` interface. For the "Hello World" field type, create the following comparison engine based on the engine for the TextLine field type. Place it in `src/FieldType/HelloWorld/Comparison/HelloWorldComparisonEngine.php`: ```php <?php declare(strict_types=1); namespace App\FieldType\HelloWorld\Comparison; use Ibexa\Contracts\VersionComparison\Engine\FieldTypeComparisonEngine; use Ibexa\Contracts\VersionComparison\FieldType\FieldTypeComparisonValue; use Ibexa\Contracts\VersionComparison\Result\ComparisonResult; final readonly class HelloWorldComparisonEngine implements FieldTypeComparisonEngine { public function __construct(private \Ibexa\VersionComparison\Engine\Value\StringComparisonEngine $stringValueComparisonEngine) { } /** * @param \App\FieldType\HelloWorld\Comparison\Value $comparisonDataA * @param \App\FieldType\HelloWorld\Comparison\Value $comparisonDataB */ public function compareFieldsTypeValues(FieldTypeComparisonValue $comparisonDataA, FieldTypeComparisonValue $comparisonDataB): ComparisonResult { return new HelloWorldComparisonResult( $this->stringValueComparisonEngine->compareValues($comparisonDataA->name, $comparisonDataB->name) ); } /** * @param \App\FieldType\HelloWorld\Comparison\Value $comparisonDataA * @param \App\FieldType\HelloWorld\Comparison\Value $comparisonDataB */ public function shouldRunComparison(FieldTypeComparisonValue $comparisonDataA, FieldTypeComparisonValue $comparisonDataB): bool { return $comparisonDataA->name->value !== $comparisonDataB->name->value; } } ``` Register the comparison engine as a service: ```yaml services: App\FieldType\HelloWorld\Comparison\HelloWorldComparisonEngine: tags: - { name: ibexa.field_type.comparable.engine, supported_type: App\FieldType\HelloWorld\Comparison\Value } ``` ## Add comparison result Next, create a comparison result class in `src/FieldType/HelloWorld/Comparison/HelloWorldComparisonResult.php`. ```php <?php declare(strict_types=1); namespace App\FieldType\HelloWorld\Comparison; use Ibexa\Contracts\VersionComparison\Result\ComparisonResult; use Ibexa\VersionComparison\Result\Value\StringComparisonResult; final readonly class HelloWorldComparisonResult implements ComparisonResult { public function __construct(private \Ibexa\VersionComparison\Result\Value\StringComparisonResult $stringDiff) { } public function getHelloWorldDiff(): StringComparisonResult { return $this->stringDiff; } public function isChanged(): bool { return $this->stringDiff->isChanged(); } } ``` ## Provide templates Finally, create a template for the new comparison view in `templates/themes/admin/field_types/field_type_comparison.html.twig`: ```html+twig {% extends '@ibexadesign/version_comparison/comparison_result_blocks.html.twig' %} {% block hello_world_field_comparison %} {% apply spaceless %} <span {{ block( 'field_attributes' ) }}> {% with { 'comparison_result': comparison_result.getHelloWorldDiff() } %} {{ block('string_diff_render') }} {% endwith %} </span> {% endapply %} {% endblock %} ``` Add configuration for this template under the `ibexa.system.<scope>.field_comparison_templates` [configuration key](https://doc.ibexa.co/en/saas/administration/configuration/configuration/#configuration-files): ```yaml ibexa: system: default: field_comparison_templates: - { template: '@ibexadesign/field_types/field_type_comparison.html.twig', priority: 10 } ``` # Customize field type metadata > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). You can customize which field type metadata should be disabled in the back office. When creating a content type definition, you add fields and configure their metadata, for example, whether they're required or translatable. If needed, you can customize that some of those options are disabled in the back office for specific field types. To do this, add custom service definition for `ModifyFieldDefinitionsCollectionTypeExtension`. For example, this configuration means that no Image field can be set as required in the definition of a content type: ```yaml services: ibexa.field_type_identifier.form.type_extension.modify_field_definitions_for_field_type_identifier_field_type: class: 'Ibexa\AdminUi\Form\Type\Extension\ModifyFieldDefinitionsCollectionTypeExtension' arguments: $fieldTypeIdentifier: 'ibexa_image' $modifiedOptions: disable_required_field: true tags: - form.type_extension ``` `fieldTypeIdentifier` refers to the identifier of the field type, in this case `ibexa_image`. `modifiedOptions` lists the changes you want to make. The following options are available: - `disable_identifier_field` - disables changing the field identifier - `disable_required_field` - disables setting the field as required - `disable_translatable_field` - disables setting the field as translatable - `disable_remove` - disables removing the field from content type definition (after it has been saved) ![Image field with disabled required option](https://doc.ibexa.co/en/saas/content_management/img/disable-required-field.png) # Field type reference > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). Cohesivo offers a range of built-in field types that cover most common needs when creating content. A field type is the underlying building block of the content model. It consists of two entities: field value and field definition. Field value is determined by values entered into the content field. Field definition is provided by the content type, and holds any user defined rules used by field type to determine how a field value is, for example, validated, stored, retrieved, or formatted. Cohesivo comes with a collection of field types that can be used to build powerful and complex content structures. In addition, it's possible to extend the system by creating custom types for special needs. > **Tip: Tip** > > For general field type documentation, see [field type](https://doc.ibexa.co/en/saas/content_management/field_types/field_types/index.md). Custom field types have to be programmed in PHP. However, the built-in field types are usually enough for typical scenarios. The following table gives an overview of the supported field types that come with Cohesivo. ## Available field types | Field type | Description | Searchable in Legacy Storage engine | Searchable with Solr/Elasticsearch | | ------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------- | | [Address](https://doc.ibexa.co/en/saas/content_management/field_types/field_type_reference/addressfield/index.md) | Stores an address. | No | No | | [Author](https://doc.ibexa.co/en/saas/content_management/field_types/field_type_reference/authorfield/index.md) | Stores a list of authors, each consisting of author name and author email. | No | Yes | | [BinaryFile](https://doc.ibexa.co/en/saas/content_management/field_types/field_type_reference/binaryfilefield/index.md) | Stores a file. | Yes | Yes | | [Checkbox](https://doc.ibexa.co/en/saas/content_management/field_types/field_type_reference/checkboxfield/index.md) | Stores a boolean value. | Yes | Yes | | [Content query](https://doc.ibexa.co/en/saas/content_management/field_types/field_type_reference/contentqueryfield/index.md) | Maps an executable repository query to a field. | No | No | | [Country](https://doc.ibexa.co/en/saas/content_management/field_types/field_type_reference/countryfield/index.md) | Stores country names as a string. | Yes[1](#1-note-on-legacy-search-engine) | Yes | | [Customer group](https://doc.ibexa.co/en/saas/content_management/field_types/field_type_reference/customergroupfield/index.md) | Stores customer group to which a user belongs. | Yes, in [Search](https://doc.ibexa.co/en/saas/search/criteria_reference/customergroupid_criterion/index.md) and [Price Search](https://doc.ibexa.co/en/saas/search/criteria_reference/price_customergroup_criterion/index.md) | Yes | | [DateAndTime](https://doc.ibexa.co/en/saas/content_management/field_types/field_type_reference/dateandtimefield/index.md) | Stores a full date including time information. | Yes | Yes | | [Date](https://doc.ibexa.co/en/saas/content_management/field_types/field_type_reference/datefield/index.md) | Stores date information. | Yes | Yes | | [EmailAddress](https://doc.ibexa.co/en/saas/content_management/field_types/field_type_reference/emailaddressfield/index.md) | Validates and stores an email address. | Yes | Yes | | [Float](https://doc.ibexa.co/en/saas/content_management/field_types/field_type_reference/floatfield/index.md) | Validates and stores a floating-point number. | No | Yes | | [Form](https://doc.ibexa.co/en/saas/content_management/field_types/field_type_reference/formfield/index.md) | Stores a form. | No | Yes | | [Image](https://doc.ibexa.co/en/saas/content_management/field_types/field_type_reference/imagefield/index.md) | Validates and stores an image. | No | Yes | | [ImageAsset](https://doc.ibexa.co/en/saas/content_management/field_types/field_type_reference/imageassetfield/index.md) | Stores images in independent content items of a generic Image content type. | No | Yes | | [Integer](https://doc.ibexa.co/en/saas/content_management/field_types/field_type_reference/integerfield/index.md) | Validates and stores an integer value. | Yes | Yes | | [ISBN](https://doc.ibexa.co/en/saas/content_management/field_types/field_type_reference/isbnfield/index.md) | Handles International Standard Book Number (ISBN) in 10-digit or 13-digit format. | Yes | Yes | | [Keyword](https://doc.ibexa.co/en/saas/content_management/field_types/field_type_reference/keywordfield/index.md) | Stores keywords. | Yes[1](#1-note-on-legacy-search-engine) | Yes | | [MapLocation](https://doc.ibexa.co/en/saas/content_management/field_types/field_type_reference/maplocationfield/index.md) | Stores map coordinates. | Yes, with [`MapLocationDistance` Criterion](https://doc.ibexa.co/en/saas/search/criteria_reference/maplocationdistance_criterion/index.md) | Yes | | [Matrix](https://doc.ibexa.co/en/saas/content_management/field_types/field_type_reference/matrixfield/index.md) | Represents and handles a table of rows and columns of data. | No | No | | [Measurement](https://doc.ibexa.co/en/saas/content_management/field_types/field_type_reference/measurementfield/index.md) | Validates and stores a unit of measure, and either a single measurement value, or a pair of range values. | Yes | Yes | | [Media](https://doc.ibexa.co/en/saas/content_management/field_types/field_type_reference/mediafield/index.md) | Validates and stores a media file. | No | Yes | | [Null](https://doc.ibexa.co/en/saas/content_management/field_types/field_type_reference/nullfield/index.md) | Used as fallback for missing field types and for testing purposes. | N/A | N/A | | [Page](https://doc.ibexa.co/en/saas/content_management/field_types/field_type_reference/pagefield/index.md) | Stores a Page with a layout consisting of multiple zones. | N/A | N/A | | [ProductSpecification](https://doc.ibexa.co/en/saas/content_management/field_types/field_type_reference/productspecificationfield/index.md) | Stores product attributes and VAT | Yes but only with [Product Search](https://doc.ibexa.co/en/saas/search/criteria_reference/product_search_criteria/index.md) | Yes | | [Relation](https://doc.ibexa.co/en/saas/content_management/field_types/field_type_reference/relationfield/index.md) | Validates and stores a relation to a content item. | Yes, with both [`Field`](https://doc.ibexa.co/en/saas/search/criteria_reference/field_criterion/index.md) and [`FieldRelation`](https://doc.ibexa.co/en/saas/search/criteria_reference/fieldrelation_criterion/index.md) Criteria | Yes | | [RelationList](https://doc.ibexa.co/en/saas/content_management/field_types/field_type_reference/relationlistfield/index.md) | Validates and stores a list of relations to content items. | Yes, with [`FieldRelation` Criterion](https://doc.ibexa.co/en/saas/search/criteria_reference/fieldrelation_criterion/index.md) | Yes | | [RichText](https://doc.ibexa.co/en/saas/content_management/field_types/field_type_reference/richtextfield/index.md) | Validates and stores structured rich text in XML. | Yes[1](#1-note-on-legacy-search-engine) | Yes | | [Selection](https://doc.ibexa.co/en/saas/content_management/field_types/field_type_reference/selectionfield/index.md) | Validates and stores a single selection or multiple choices from a list of options. | Yes[1](#1-note-on-legacy-search-engine) | Yes | | [TaxonomyEntry](https://doc.ibexa.co/en/saas/content_management/field_types/field_type_reference/taxonomyentryfield/index.md) | Stores information about the Taxonomy tree. | No | Yes | | [TaxonomyEntryAssignment](https://doc.ibexa.co/en/saas/content_management/field_types/field_type_reference/taxonomyentryassignmentfield/index.md) | Makes content taggable by Taxonomy. | No | Yes | | [TextBlock](https://doc.ibexa.co/en/saas/content_management/field_types/field_type_reference/textblockfield/index.md) | Validates and stores a larger block of text. | Yes[1](#1-note-on-legacy-search-engine) | Yes | | [TextLine](https://doc.ibexa.co/en/saas/content_management/field_types/field_type_reference/textlinefield/index.md) | Validates and stores a single line of text. | Yes | Yes | | [Time](https://doc.ibexa.co/en/saas/content_management/field_types/field_type_reference/timefield/index.md) | Stores time information. | Yes | Yes | | [Url](https://doc.ibexa.co/en/saas/content_management/field_types/field_type_reference/urlfield/index.md) | Stores a URL / address. | No | Yes | | [User](https://doc.ibexa.co/en/saas/content_management/field_types/field_type_reference/userfield/index.md) | Validates and stores information about a user. | No | No | **[1] Note on Legacy Search Engine** Legacy Search/Storage Engine index is limited to 255 characters in database design, so formatted and unformatted text blocks only index the first part. In case of multiple selection field types like, for example, Keyword, Selection, or Country, only the first choices are indexed. they're indexed only as a text blob separated by string separator. Proper indexing of these field types is done with [Solr Search engine](https://doc.ibexa.co/en/saas/search/search_engines/solr_search_engine/solr_overview/index.md). # Address field type > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). Editions: Experience This field represents and handles address fields. It allows you to customize address fields per country. | Name | Internal name | Expected input | | --------- | --------------- | --------------------------- | | `Address` | `ibexa_address` | `string`, `string`, `array` | The Address field type is available via the Address Bundle provided by the `ibexa/fieldtype-address` package. ## PHP API field type ### Inputs | Type | Description | Example | | -------- | --------------------------------------------- | ----------------- | | `string` | Name of the address. | `My home address` | | `string` | Country code in ISO 3166-1 alpha-2 format. | `PL` | | `array` | Additional fields, defined by address format. | see below | ### Example input ```php use Ibexa\FieldTypeAddress\FieldType; new FieldType\Value( 'My home address', 'PL', [ 'city' => 'Warsaw', 'region' => 'Masovian', 'postal_code' => '11-123', ] ); ``` ### Validation This field type validates whether `Country` and `Name` fields have been filled out. ### Value object #### Properties | Property | Type | Description | | ---------- | -------- | --------------------------------------------- | | `$name` | `string` | Name of the address. | | `$country` | `string` | Country code in ISO 3166-1 alpha-2 format. | | `$fields` | `array` | Additional fields, defined by address format. | #### Constructor See above (Example input). ### Formats The following default configuration defines default fields for `personal` address type: ```yaml formats: personal: country: default: - region - locality - street - postal_code ``` #### Modifying field configuration ```yaml formats: billing_address: country: DE: - tax_number - city - address - postal_code ``` Adds (or alters) an address format for `DE` country of `billing_address` type. ### Field form types By default, each field is a simple text input with a label made of field identifier. To change the type of field, you need to listen to a specific event. For each field below events are dispatched (in order): ```yaml ibexa.address.field.{FIELD_IDENTIFIER} ibexa.address.field.{FIELD_IDENTIFIER}.{ADDRESS_TYPE} ibexa.address.field.{FIELD_IDENTIFIER}.{ADDRESS_TYPE}.{COUNTRY_CODE} ``` #### Example ```yaml ibexa.address.field.tax_number ibexa.address.field.tax_number.billing_address ibexa.address.field.tax_number.billing_address.DE ``` #### Example event listener An event listener can also provide validation by using either one of [constraints provided by Symfony](https://symfony.com/doc/7.4/validation.html#supported-constraints), or a custom constraint. ```php use Ibexa\Contracts\FieldTypeAddress\Event\MapFieldEvent; use Symfony\Component\EventDispatcher\EventSubscriberInterface; use Symfony\Component\Form\Extension\Core\Type\IntegerType; use Symfony\Component\Validator\Constraints\Positive; class ExampleAddressSubscriber implements EventSubscriberInterface { public static function getSubscribedEvents(): array { return [ 'ibexa.address.field.tax_number.billing_address' => 'onBillingAddressTaxNumber', ]; } public function onBillingAddressTaxNumber(MapFieldEvent $event): void { $event->setLabel('VAT'); $event->setType(IntegerType::class); $event->setOptions([ 'attr' => ['class' => 'some-tax-number'], 'constraints' => [new Positive()], ]); } } ``` # Author field type > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). This field type allows the storage and retrieval of one or more authors. For each author, it can handle a name and an email address. It's typically used to store information about additional authors who have written/created different parts of a content item. | Name | Internal name | Expected input | Output | | -------- | -------------- | -------------- | -------- | | `Author` | `ibexa_author` | mixed | `string` | ## PHP API field type ### Value object #### Properties | Attribute | Type | Description | Example | | --------- | --------------------------------------- | ---------------- | --------- | | `authors` | `\Ibexa\Core\FieldType\Author\Author[]` | List of authors. | See below | Example: ```php use Ibexa\Core\FieldType\Author; $authorList = new Author\Value([ new Author\Author([ 'id' => 1, 'name' => 'Boba Fett', 'email' => 'boba.fett@example.com', ]), new Author\Author([ 'id' => 2, 'name' => 'Darth Vader', 'email' => 'darth.vader@example.com', ]), ]); ``` #### Hash format The hash format mostly matches the value object. It has the following key `authors`. Example ```php [ [ 'id' => 1, 'name' => 'Boba Fett', 'email' => 'boba.fett@example.com', ], [ 'id' => 2, 'name' => 'Darth Vader', 'email' => 'darth.vader@example.com', ], ]; ``` #### String representation The string contains all the authors with their names and emails. Example: `John Doe john@doe.com` ### Validation This field type doesn't perform any special validation of the input value. ### Settings The Field definition of this field type can be configured with a single option: | Name | Type | Default value | Description | | --------------- | ------- | --------------------------- | --------------------------------------------------------------------------------------------------------------------------------------- | | `defaultAuthor` | `mixed` | `Type::DEFAULT_VALUE_EMPTY` | One of the `DEFAULT_*` constants, used by the administration interface for setting the default Field value. See below for more details. | Following `defaultAuthor` default value options are available as constants in the `Ibexa\Core\FieldType\Author\Type` class: | Constant | Description | | ---------------------- | ----------------------------------------- | | `DEFAULT_VALUE_EMPTY` | Default value is empty. | | `DEFAULT_CURRENT_USER` | Default value uses currently logged user. | ```php // Author field type example settings use Ibexa\Core\FieldType\Author\Type; $settings = [ 'defaultAuthor' => Type::DEFAULT_VALUE_EMPTY, ]; ``` # BinaryFile field type > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). This field type represents and handles a single binary file. It also counts the number of times the file has been downloaded from the `content/download` module. It's capable of handling virtually any file type and is typically used for storing legacy document types, for example, PDF files, Word documents, or spreadsheets. The maximum allowed file size is determined by the "Max file size" class attribute edit parameter and the `upload_max_filesize` directive in the main PHP configuration file (`php.ini`). | Name | Internal name | Expected input | Output | | ------------ | ------------------ | -------------- | ------ | | `BinaryFile` | `ibexa_binaryfile` | mixed | mixed | ## PHP API field type ### Value object #### Properties Both `BinaryFile` and `Media` Value and Type inherit from the `BinaryBase` abstract field type, and share common properties. `Ibexa\Core\FieldType\BinaryFile\Value` offers the following properties: | Attribute | Type | Description | Example | | --------------- | ------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------- | | `id` | string | Binary file identifier. This ID depends on the [IO Handler](https://doc.ibexa.co/en/saas/infrastructure_and_maintenance/clustering/clustering/#dfs-io-handler) that is being used. With the native, default handlers (FileSystem and Legacy), the ID is the file path, relative to the binary file storage root dir (`var/<vardir>/storage/original` by default). | application/63cd472dd7.pdf | | `fileName` | string | The human-readable file name, as exposed to the outside. Used when sending the file for download to name the file. | 20130116_whitepaper.pdf | | `fileSize` | int | File size, in bytes. | 1077923 | | `mimeType` | string | The file's MIME type. | application/pdf | | `uri` | string | The binary file's `content/download` URI. If the URI doesn't include a host or protocol, it applies to the request domain. | /content/download/210/2707 | | `downloadCount` | integer | Number of times the file was downloaded | 0 | | `inputUri` | string | Path to a local file when creating a field value, `null` when reading a field value | `path/to/document.pdf` | #### Constructor's hash format The hash format mostly matches the value object. It has the following keys: | Key | Status | Type | Description | | --------------- | ---------- | ------- | ---------------------------------------------------------------------------------------- | | `inputUri` | mandatory | string | Path to the local file to be uploaded into the field. | | `id` | deprecated | string | Backward compatibility alias for `inputUri`. | | `path` | deprecated | string | Backward compatibility alias for `inputUri`. | | `fileName` | optional | string | Name of the file when downloaded. If not given, the basename of `inputUri` is used | | `fileSize` | optional | integer | Size of the file in bytes. If not given, the size of the `inputUri` target file is used. | | `downloadCount` | optional | integer | Number of times the file was downloaded. If not given, set to `0` (zero). | | `mimeType` | ignored | | | | `uri` | ignored | | | Example: ```php /** @var \Ibexa\Contracts\Core\Repository\Values\Content\ContentCreateStruct $fileContentCreateStruct */ $fileContentCreateStruct->setField('file', new Ibexa\Core\FieldType\BinaryFile\Value([ 'fileName' => 'example.pdf', 'inputUri' => '/tmp/example_for_website.pdf', ])); ``` The original local file name `example_for_website.pdf` is forgotten. When downloaded, the filename is `example.pdf`. To use a remote file, you have to download it locally first, then remove it after it's used in `ContentService::createContent`. ## REST API specifics Used in the REST API, a BinaryFile field mostly serializes the hash described above. However there are a couple specifics worth mentioning. ### Reading content: `url` property When reading the contents of a field of this type, an extra key is added: `url`. This key gives you the absolute file URL, protocol and host included. Example: `http://example.com/var/ezdemo_site/storage/original/application/63cd472dd7819da7b75e8e2fee507c68.pdf` ### Creating content: `data` property When creating BinaryFile content with the REST API, it's possible to provide data as a base64 encoded string, by using the `data` fieldValue key: ```xml <field> <fieldDefinitionIdentifier>file</fieldDefinitionIdentifier> <languageCode>eng-GB</languageCode> <fieldValue> <value key="fileName">My file.pdf</value> <value key="fileSize">17589</value> <value key="data"><![CDATA[/9j/4AAQSkZJRgABAQEAZABkAAD/2wBDAAIBAQIBAQICAgICAgICAwUDAwMDAwYEBAMFBwYHBwcG ... ...]]></value> </fieldValue> </field> ``` # Checkbox field type > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). The Checkbox field type stores the current status for a checkbox input, checked or unchecked, by storing a boolean value. | Name | Internal name | Expected input type | | ---------- | --------------- | ------------------- | | `Checkbox` | `ibexa_boolean` | `boolean` | ## PHP API field type ### Value object #### Properties The Value class of this field type contains the following properties: | Property | Type | Default value | Description | | -------- | --------- | ------------- | ------------------------------------------------------------------------------ | | `$bool` | `boolean` | `false` | This property is used for the checkbox status, represented by a boolean value. | ```php //Value object content examples use Ibexa\Core\FieldType\Checkbox; // Instantiates a checkbox value with a default state (false) $checkboxValue = new Checkbox\Value(); // Checked $checkboxValue->bool = true; // Unchecked $checkboxValue->bool = false; ``` ##### Constructor The `Checkbox\Value` constructor accepts a boolean value: ```php // Constructor example use Ibexa\Core\FieldType\Checkbox; // Instantiates a checkbox value with a checked state $checkboxValue = new Checkbox\Value(true); ``` ##### String representation As this field type isn't a string but a boolean, it returns "1" (true) or "0" (false) in cases where it's cast to string, and it's never considered empty. # Content query field type > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). This field type maps an executable repository query to a field. | Name | Internal name | Expected input | | ------- | --------------------- | -------------- | | `Query` | `ibexa_content_query` | `string` | The Content query field type is available via the Query field type Bundle provided by the [fieldtype-query](https://github.com/ibexa/fieldtype-query) package. For information about the field type's usage, see [Content queries](https://doc.ibexa.co/en/saas/templating/queries_and_controllers/content_queries/#content-query-field). # Country field type > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). This field type represents one or multiple countries. | Name | Internal name | Expected input | | --------- | --------------- | -------------- | | `Country` | `ibexa_country` | `array` | ## PHP API field type ### Input expectations Example array: ```php [ 'JP' => [ 'Name' => 'Japan', 'Alpha2' => 'JP', 'Alpha3' => 'JPN', 'IDC' => 81, ], ]; ``` When you set an array directly on a content field you don't need to provide all this information, the field type assumes it's a hash and in this case accepts a simplified structure described below under [Hash format](#hash-format). ### Validation This field type validates whether multiple countries are allowed by the field definition, and whether the [Alpha2](https://www.iso.org/iso-3166-country-codes.html) is valid according to the countries configured in Cohesivo. ### Settings The field definition of this field type can be configured with one option: | Name | Type | Default value | Description | | ------------ | --------- | ------------- | ------------------------------------------------------------------------------------------ | | `isMultiple` | `boolean` | `false` | This setting allows (if true) or prohibits (if false) the selection of multiple countries. | ```php // Country FieldType example settings $settings = [ 'isMultiple' => true, ]; ``` ### Hash format The format used for serialization is simpler than the full format. It's also available when setting value on the content field, by setting the value to an array instead of the value object. Example of that shown below: ```php // Value object content example /** @var \Ibexa\Contracts\Core\Repository\Values\Content\Content $content */ $content->fields['countries'] = ['JP', 'NO']; ``` The format used by the toHash method is the Alpha2 value, however the input is capable of accepting either Name, Alpha2, or Alpha3 value as shown below in the value object section. ### Value object #### Properties The Value class of this field type contains the following properties: | Property | Type | Description | | ------------ | --------- | ------------------------------------------------------------------------------------- | | `$countries` | `array[]` | This property is used for the country selection provided as input, as its attributes. | ```php // Value object content example /** @var \Ibexa\Core\FieldType\Country\Value $value */ $value->countries = [ 'JP' => [ 'Name' => 'Japan', 'Alpha2' => 'JP', 'Alpha3' => 'JPN', 'IDC' => 81, ], ]; ``` ##### Constructor The `Country\Value` constructor initializes a new value object with the value provided. It expects an array as input. ```php // Constructor example use Ibexa\Core\FieldType\Country as Country; // Instantiates a Country Value object $countryValue = new Country\Value( [ 'JP' => [ 'Name' => 'Japan', 'Alpha2' => 'JP', 'Alpha3' => 'JPN', 'IDC' => 81, ], ] ); ``` # Customer group field > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). This field type represents a customer group that a user belongs to. | Name | Internal name | Expected input type | | ---------------- | ---------------------- | ------------------- | | `Customer group` | `ibexa_customer_group` | `int` or null | ## PHP API field type ### Value object #### Properties The Value class of this field type contains the following properties: | Property | Type | Description | | -------- | ----- | ------------------------- | | `$id` | `int` | ID of the customer group. | # DateAndTime field type > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). This field type represents a full date and time information. | Name | Internal name | Expected input type | | ------------- | ---------------- | ------------------- | | `DateAndTime` | `ibexa_datetime` | mixed | ## PHP API field type ### Input expectations If input value is of type `string` or `integer`, it's passed directly to the [PHP's built-in `\DateTime` class constructor](https://www.php.net/manual/en/datetime.construct.php), therefore the same input format expectations apply. It's also possible to directly pass an instance of `\DateTime`. | Type | Example | | ----------- | ---------------------------------- | | `integer` | `"2017-08-28 12:20 Europe/Berlin"` | | `integer` | `1346149200` | | `\DateTime` | `new \DateTime()` | ### Value object #### Properties The Value class of this field type contains the following properties: | Property | Type | Description | | -------- | ----------- | ------------------------------------------------------ | | `$value` | `\DateTime` | The date and time value as an instance of `\DateTime`. | ##### Constructor The constructor for this value object initializes a new value object with the value provided. It accepts an instance of PHP's built-in `\DateTime` class. ##### String representation String representation of the date value generates the date string in the format `D Y-d-m H:i:s` as accepted by [PHP's built-in `date()` function](https://www.php.net/manual/en/function.date.php). | Character | Description | Example | | --------- | ------------------------------------------------------------------- | ------- | | D | Three letter representation of a day, range Mon to Sun | Wed | | Y | Four digit representation of a year | 2016 | | d | Two digit representation of a day, range 01 to 31 | 22 | | m | Two digit representation of a month, range 01 to 12 | 05 | | H | Two digit representation of an hour, 24-hour format, range 00 to 23 | 12 | | i | Two digit representation of minutes, range 00 to 59 | 19 | | s | Two digit representation of seconds, range 00 to 59 | 18 | Example: `Wed 2016-22-05 12:19:18` ### Hash format Hash value of this field type is an array with two keys: | Key | Type | Description | Example | | ----------- | --------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ | --------------------------------------- | | `timestamp` | `integer` | Time information in [Unix format timestamp](https://en.wikipedia.org/wiki/Unix_time). | `1400856992` | | `rfc850` | `string` | Time information as a string in [RFC 850 date format](https://datatracker.ietf.org/doc/html/rfc850). As input, this has precedence over the timestamp value. | `"Friday, 23-May-14 14:56:14 GMT+0000"` | ```php $hash = [ 'timestamp' => 1400856992, 'rfc850' => 'Friday, 23-May-14 14:56:14 GMT+0000', ]; ``` ### Validation This field type doesn't perform any special validation of the input value. ### Settings The field definition of this field type can be configured with several options: | Name | Type | Default value | Description | | -------------- | ---------------- | --------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `useSeconds` | `boolean` | `false` | Used to control displaying of seconds in the output. | | `defaultType` | `mixed` | `Type::DEFAULT_EMPTY` | One of the `DEFAULT_*` constants, used by the administration interface for setting the default field value. See below for more details. | | `dateInterval` | `?\DateInterval` | `null` | This setting complements `defaultType` setting and can be used only when the latter is set to `Type::DEFAULT_CURRENT_DATE_ADJUSTED`. In that case the default input value when using administration interface is adjusted by the given `\DateInterval`. | Following `defaultType` default value options are available as constants in the `Ibexa\Core\FieldType\DateAndTime\Type` class: | Constant | Description | | ------------------------------- | -------------------------------------------------------------------------------------------- | | `DEFAULT_EMPTY` | Default value is empty. | | `DEFAULT_CURRENT_DATE` | Default value uses current date. | | `DEFAULT_CURRENT_DATE_ADJUSTED` | Default value uses current date, adjusted by the interval defined in `dateInterval` setting. | ```php // DateAndTime FieldType example settings use Ibexa\Core\FieldType\DateAndTime\Type; $settings = [ 'useSeconds' => false, 'defaultType' => Type::DEFAULT_EMPTY, 'dateInterval' => null, ]; /** @var \Ibexa\Contracts\Core\Repository\ContentTypeService $contentTypeService */ $dateAndTimeFieldCreateStruct = $contentTypeService->newFieldDefinitionCreateStruct( 'my_date_and_time_field', 'ibexa_datetime' ); $dateAndTimeFieldCreateStruct->fieldSettings = $settings; ``` # Date field type > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). This field type represents a date without time information. | Name | Internal name | Expected input type | | ------ | ------------- | ------------------- | | `Date` | `ibexa_date` | mixed | ## PHP API field type ### Input expectations If input value is in `string` or `integer` format, it's passed directly to [PHP's built-in `\DateTime` class constructor](https://www.php.net/manual/en/datetime.construct.php), therefore the same input format expectations apply. It's also possible to directly pass an instance of `\DateTime`. | Type | Example | | ----------- | ---------------------------------- | | `string` | `"2012-08-28 12:20 Europe/Berlin"` | | `integer` | `1346149200` | | `\DateTime` | `new \DateTime()` | Time information is **not stored**. Before storing, the provided input value is set to the beginning of the day in the given or the environment timezone. ### Value object #### Properties The Value class of this field type contains the following properties: | Property | Type | Description | | -------- | ----------- | ------------------------------------------- | | `$date` | `\DateTime` | This property is used for the text content. | ##### String representation String representation of the date value generates the date string in the format "l d F Y" as accepted by [PHP's built-in `date()` function](https://www.php.net/manual/en/function.date.php). | Character | Description | Example | | --------- | ------------------------------------------------------------------- | --------- | | l | Textual representation of a day of the week, range Monday to Sunday | Wednesday | | d | Two digit representation of a day, range 01 to 31 | 22 | | F | Textual representation of a month, range January to December | May | | Y | Four digit representation of a year | 2016 | Example: `Wednesday 22 May 2016` ##### Constructor The constructor for this value object initializes a new value object with the value provided. It accepts an instance of [PHP's built-in `\DateTime` class](https://www.php.net/manual/en/datetime.construct.php). ### Hash format Hash value of this field type is an array with two keys: | Key | Type | Description | Example | | ----------- | --------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------- | | `timestamp` | `integer` | Time information in [Unix format timestamp](https://en.wikipedia.org/wiki/Unix_time). | `1400856992` | | `rfc850` | `string` | Time information as a string in [RFC 850 date format](https://datatracker.ietf.org/doc/html/rfc850). As input, this has higher precedence over the timestamp value. | `"Friday, 23-May-14 14:56:14 GMT+0000"` | ```php // Example of the hash value in PHP $hash = [ 'timestamp' => 1400856992, 'rfc850' => 'Friday, 23-May-14 14:56:14 GMT+0000', ]; ``` ### Validation This field type doesn't perform any special validation of the input value. ### Settings The field definition of this field type can be configured with a single option: | Name | Type | Default value | Description | | ------------- | ------- | --------------------- | --------------------------------------------------------------------------------------------------------------------------------------- | | `defaultType` | `mixed` | `Type::DEFAULT_EMPTY` | One of the `DEFAULT_*` constants, used by the administration interface for setting the default field value. See below for more details. | Following `defaultType` default value options are available as constants in the `Ibexa\Core\FieldType\Date\Type` class: | Constant | Description | | ---------------------- | -------------------------------- | | `DEFAULT_EMPTY` | Default value is empty. | | `DEFAULT_CURRENT_DATE` | Default value uses current date. | ```php // Date field type example settings use Ibexa\Core\FieldType\Date\Type; $settings = [ 'defaultType' => Type::DEFAULT_EMPTY, ]; ``` # EmailAddress field type > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). The EmailAddress field type represents an email address, in the form of a string. | Name | Internal name | Expected input type | | -------------- | ------------- | ------------------- | | `EmailAddress` | `ibexa_email` | `string` | ## PHP API field type ### Value object #### Properties The `Value` class of this field type contains the following properties: | Property | Type | Description | | -------- | -------- | --------------------------------------------------------------------- | | `$email` | `string` | This property is used for the input string provided as email address. | ```php // Value object content example use Ibexa\Core\FieldType\EmailAddress\Value; // Instantiates an EmailAddress Value object with default value (empty string) $emailaddressValue = new Value(); // Email definition $emailaddressValue->email = 'someuser@example.com'; ``` ##### Constructor The `EmailAddress\Value` constructor initializes a new value object with the value provided. It accepts a string as input. ```php // Constructor example use Ibexa\Core\FieldType\EmailAddress\Value; // Instantiates an EmailAddress Value object $emailaddressValue = new Value('someuser@example.com'); ``` ##### String representation String representation of the field type's value object is the email address contained in it. Example: `someuser@example.com` ### Hash format Hash value for this field type's Value is simply the email address as a string. Example: `someuser@example.com` ### Validation This field type uses the `EmailAddressValidator` validator as a resource which tests the string supplied as input against a pattern, to make sure that a valid email address has been provided. If the validations fail, a `ValidationError` is thrown, specifying the error message. ### Settings This field type doesn't support settings. # Float field type > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). This field type stores numeric values which are provided as floats. | Name | Internal name | Expected input | | ------- | ------------- | -------------- | | `Float` | `ibexa_float` | `float` | ## PHP API field type ### Input expectations The field type expects a number as input. Both decimal and integer numbers are accepted. | Type | Example | | ------- | ------------ | | `float` | `194079.572` | | `int` | `144` | ### Value object #### Properties The Value class of this field type contains the following properties: | Property | Type | Description | | -------- | ------- | ------------------------------------------------------------- | | `$value` | `float` | This property is used to store the value provided as a float. | ```php // Value object content example use Ibexa\Core\FieldType\Float\Value as FloatValue; // Instantiates a Float Value object $floatValue = new FloatValue(); $floatValue->value = 284.773; ``` ##### Constructor The `Float\Value` constructor initializes a new value object with the value provided. It expects a numeric value with or without decimals. ```php // Constructor example use Ibexa\Core\FieldType\Float\Value as FloatValue; // Instantiates a Float Value object $floatValue = new FloatValue(284.773); ``` ### Validation This field type supports `FloatValueValidator`, defining maximum and minimum float value: | Name | Type | Default value | Description | | --------------- | ------- | ------------- | --------------------------------------------------------------------------------- | | `minFloatValue` | `float` | \`null | This setting defines the minimum value this field type which is allowed as input. | | `maxFloatValue` | `float` | \`null | This setting defines the maximum value this field type which is allowed as input. | ```php // Validator configuration example in PHP /** @var \Ibexa\Contracts\Core\Repository\Repository $repository */ $contentTypeService = $repository->getContentTypeService(); $floatFieldCreateStruct = $contentTypeService->newFieldDefinitionCreateStruct('float', 'ibexa_float'); // Accept only numbers between 0.1 and 203.99 $floatFieldCreateStruct->validatorConfiguration = [ 'FileSizeValidator' => [ 'minFloatValue' => 0.1, 'maxFloatValue' => 203.99, ], ]; ``` ### Settings This field type doesn't support settings. # Form field type > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). Editions: Experience The Form field type stores a Form consisting of one or more form fields. | Name | Internal name | | ------ | ------------- | | `Form` | `ibexa_form` | For more information about working with Forms, see [Forms](https://doc.ibexa.co/en/saas/content_management/forms/work_with_forms/index.md). # Image field type > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). The Image field type allows you to store an image file. | Name | Internal name | | ------- | ------------- | | `Image` | `ibexa_image` | A **variation service** handles the conversion of the original image into different formats and sizes through a set of preconfigured named variations, for example, large, small, medium, or black and white thumbnail. ## PHP API field type ### Value object The `value` property of an Image field returns an `Ibexa\Core\FieldType\Image\Value` object with the following properties: #### Properties | Property | Type | Example | Description | | ----------------- | ------ | ---------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `id` | string | `0/8/4/1/1480-1-eng-GB/image.png` | The image's unique identifier. Usually the path, or a part of the path. To get the full path, use the `uri` property. | | `alternativeText` | string | `Picture of an apple.` | The alternative text, as entered in the field's properties. This property is optional. It's recommended that you require the alternative text for an image when you add the Image field to a content type, by selecting the "Alternative text is required" checkbox. | | `fileName` | string | `image.png` | The original image's filename, without the path. | | `fileSize` | int | `37931` | The original image's size, in bytes. | | `uri` | string | `var/ezdemo_site/storage/images/0/8/4/1/1480-1-eng-GB/image.png` | The original image's URI. | | `imageId` | string | `240-1480` | A special image ID, used by REST. | | `inputUri` | string | `var/storage/images/test/199-2-eng-GB/image.png` | Input image file URI. | | `width` | int | `960` | Original image width in pixels. | | `height` | int | `540` | Original image height in pixels. | ### Settings This field type doesn't support settings. ### Image variations Using the variation Service, variations of the original image can be obtained. They're `Ibexa\Contracts\Core\Variation\Values\ImageVariation` objects with the following properties: | Property | Type | Example | Description | | -------------- | -------- | ------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------ | | `width` | int | `200` | The variation's width in pixels. | | `height` | int | `112` | The variation's height in pixels. | | `name` | string | `medium` | The variation's identifier, name of the image variation. | | `info` | mixed | n/a | Extra information about the image, depending on the image type, such as EXIF data. If there is no information, the `info` value is `null`. | | `fileSize` | int | `31010` | Size (in byte) of current variation. | | `mimeType` | string | `image/png` | The MIME type. | | `fileName` | string | `my_image.png` | The name of the file. | | `dirPath` | string | `var/storage/images/test/199-2-eng-GB` | The path to the file. | | `uri` | string | `var/storage/images/test/199-2-eng-GB/apple.png` | The variation's URI. Complete path with a name of image file. | | `lastModified` | DateTime | `"2017-08-282 12:20 Europe/Berlin"` | When the variation was last modified. | ### Field Definition options The Image field type supports one `FieldDefinition` option: the maximum size for the file. > **Note: Note** > > Maximum size is 10MB. We recommend setting the `upload_max_filesize` key in the `php.ini` configuration file to a value equal to or higher than that. It prevents validation errors while editing content types. ## Using an Image field To read more about handling images and image variations, see the [Images documentation](https://doc.ibexa.co/en/saas/content_management/images/images/index.md). ### With the REST API Image Fields within REST are exposed by the `application/vnd.ibexa.api.Content` media-type. An Image field looks like this: ```xml <field> <id>1480</id> <fieldDefinitionIdentifier>image</fieldDefinitionIdentifier> <languageCode>eng-GB</languageCode> <fieldValue> <value key="inputUri">/var/ezdemo_site/storage/images/0/8/4/1/1480-1-eng-GB/kidding.png</value> <value key="alternativeText"></value> <value key="fileName">kidding.png</value> <value key="fileSize">37931</value> <value key="imageId">240-1480</value> <value key="uri">/var/ezdemo_site/storage/images/0/8/4/1/1480-1-eng-GB/kidding.png</value> <value key="variations"> <value key="articleimage"> <value key="href">/api/ibexa/v2/content/binary/images/240-1480/variations/articleimage</value> </value> <value key="articlethumbnail"> <value key="href">/api/ibexa/v2/content/binary/images/240-1480/variations/articlethumbnail</value> </value> </value> </fieldValue> </field> ``` Children of the `fieldValue` node list the general properties of the field's original image (for example, `fileSize`, `fileName`, or `inputUri`), and its variations. For each variation, a URI is provided. Requested through REST, this resource generates the variation if it doesn't exist yet, and list the variation details: ```xml <ContentImageVariation media-type="application/vnd.ibexa.api.ContentImageVariation+xml" href="/api/ibexa/v2/content/binary/images/240-1480/variations/tiny"> <uri>/var/ezdemo_site/storage/images/0/8/4/1/1480-1-eng-GB/kidding_tiny.png</uri> <contentType>image/png</contentType> <width>30</width> <height>30</height> <fileSize>1361</fileSize> </ContentImageVariation> ``` ### From REST The REST API expects field values to be provided in a hash-like structure. Those keys are identical to those expected by the `Image\Value` constructor: `fileName`, `alternativeText`. In addition, image data can be provided using the `data` property, with the image's content encoded as base64. #### Creating an Image field ```xml <?xml version="1.0" encoding="UTF-8"?> <ContentCreate> <!-- [...metadata...] --> <fields> <field> <id>247</id> <fieldDefinitionIdentifier>image</fieldDefinitionIdentifier> <languageCode>eng-GB</languageCode> <fieldValue> <value key="fileName">rest-rocks.jpg</value> <value key="alternativeText">HTTP</value> <value key="data"><![CDATA[/9j/4AAQSkZJRgABAQEAZABkAAD/2wBDAAIBAQIBAQICAgICAgICAwUDAwMDAwYEBAMFBwYHBwcG BwcICQsJCAgKCAcHCg0KCgsMDAwMBwkODw0MDgsMDAz/2[...]</value> </fieldValue> </field> </fields> </ContentCreate> ``` ### Updating an Image field Updating an Image field requires that you re-send existing data. This can be done by re-using the field obtained via REST, **removing the variations key**, and updating `alternativeText`, `fileName` or `data`. If you don't want to change the image itself, don't provide the `data` key. ```xml <?xml version="1.0" encoding="UTF-8"?> <VersionUpdate> <fields> <field> <id>247</id> <fieldDefinitionIdentifier>image</fieldDefinitionIdentifier> <languageCode>eng-GB</languageCode> <fieldValue> <value key="id">media/images/507-1-eng-GB/Existing-image.png</value> <value key="alternativeText">Updated alternative text</value> <value key="fileName">Updated-filename.png</value> </fieldValue> </field> </fields> </VersionUpdate> ``` # ImageAsset field type > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). Image Asset field type enables storing images in independent content items of a generic Image content type, in the media library. It makes them reusable across system. | Name | Internal name | | ------------ | ------------------- | | `ImageAsset` | `ibexa_image_asset` | ## Input expectations Example array: | Type | Description | Example | | ------------------------------------------------------------ | ----------------------------------------------- | ---------- | | `Ibexa\Core\FieldType\ImageAsset\Value` | Image Asset field type value object. | See below. | | `Ibexa\Contracts\Core\Repository\Values\Content\ContentInfo` | ContentInfo instance of the Asset content item. | n/a | | `string` | ID of the Asset content item. | `"150"` | | `integer` | ID of the Asset content item. | `150` | ## Value object ### Properties Value object of `ibexa_image_asset` contains the following properties: | Property | Type | Description | | ---------------------- | -------- | ---------------------------------------------------------------- | | `destinationContentId` | `int` | Related content ID. | | `alternativeText` | `string` | The alternative image text (for example "Picture of an apple."). | ```php /** * Value object content example. * * @var \Ibexa\Core\FieldType\ImageAsset\Value $imageAssetValue * @var \Ibexa\Contracts\Core\Repository\Values\Content\ContentInfo $contentInfo */ $imageAssetValue->destinationContentId = $contentInfo->id; $imageAssetValue->alternativeText = 'Picture of an apple.'; ``` #### Constructor The `ImageAsset\Value` constructor initializes a new value object with the value provided. It expects an ID of a content item representing asset and the alternative text. ```php // Constructor example use Ibexa\Core\FieldType\ImageAsset as ImageAsset; /** @var \Ibexa\Contracts\Core\Repository\Values\Content\ContentInfo $contentInfo */ // Instantiates a ImageAsset Value object $imageAssetValue = new ImageAsset\Value($contentInfo->id, 'Picture of an apple.'); ``` ### Validation This field type validates if: - `destinationContentId` points to a content item which has correct content type ## Configuration ImageAsset field type allows configuring the following options: | Name | Description | Default value | | -------------------------- | -------------------------------------- | ------------- | | `content_type_identifier` | Content type used to store assets. | `image` | | `content_field_identifier` | Field identifier used for asset data. | `image` | | `name_field_identifier` | Field identifier used for asset name. | `name` | | `parent_location_id` | Location where the assets are created. | `51` | Example configuration: ```yaml ibexa: system: default: fieldtypes: ibexa_image_asset: content_type_identifier: photo content_field_identifier: image name_field_identifier: title parent_location_id: 106 ``` ## Customizing ImageAsset field type rendering Internally, the Image Asset Type is rendered via subrequest (similar to other relation types). Rendering customization is possible by configuring view type `asset_image`: ```yaml ibexa: system: default: content_view: asset_image: default: template: ::custom_image_asset_template.html.twig match: [] ``` ## Generating image variation from the Image Asset Thanks to the `Ibexa\Bundle\Core\Imagine\ImageAsset` decorator you can work with `Ibexa\Contracts\Core\Variation` in the same way as with [Image field type](https://doc.ibexa.co/en/saas/content_management/field_types/field_type_reference/imagefield/index.md). # Integer field type > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). This field type represents an integer value. | Name | Internal name | Expected input | | --------- | --------------- | -------------- | | `Integer` | `ibexa_integer` | `integer` | ## PHP API field type ### Input expectations | Type | Example | | --------- | ------- | | `integer` | `2397` | ### Value object #### Properties The Value class of this field type contains the following properties: | Property | Type | Description | | -------- | ----- | ---------------------------------------------------------------- | | `$value` | `int` | This property is used to store the value provided as an integer. | ```php // Value object content example /** @var \Ibexa\Core\FieldType\Integer\Value $integer */ $integer->value = 8; ``` #### Constructor The `Integer\Value` constructor initializes a new value object with the value provided. It expects a numeric, integer value. ```php // Constructor example use Ibexa\Core\FieldType\Integer; // Instantiates a Integer Value object $integerValue = new Integer\Value(8); ``` #### Hash format Hash value of this field type is an integer value as a string. Example: `"8"` #### String representation String representation of the field type's value returns the integer value as a string. Example: `"8"` ### Validation This field type supports `IntegerValueValidator`, defining maximum and minimum float value: | Name | Type | Default value | Description | | ----------------- | ----- | ------------- | --------------------------------------------------------------------------------- | | `minIntegerValue` | `int` | `0` | This setting defines the minimum value this field type which is allowed as input. | | `maxIntegerValue` | `int` | `null` | This setting defines the maximum value this field type which is allowed as input. | ```php // Example of validator configuration in PHP $validatorConfiguration = [ 'minIntegerValue' => 1, 'maxIntegerValue' => 24, ]; ``` ### Settings This field type doesn't support settings. # ISBN field type > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). This field type represents an ISBN string either an ISBN-10 or ISBN-13 format. | Name | Internal name | Expected input type | | ------ | ------------- | ------------------- | | `ISBN` | `ibexa_isbn` | `string` | ## PHP API field type ### Value object #### Properties The Value class of this field type contains the following properties: | Property | Type | Description | | -------- | -------- | ------------------------------------------ | | `$isbn` | `string` | This property is used for the ISBN string. | #### String representation An ISBN's string representation is the `$isbn` property's value, as a string. #### Constructor The constructor for this value object initializes a new value object with the value provided. It accepts a string as argument and sets it to the `isbn` attribute. ### Validation The input passed into this field type is subject of ISBN validation depending on the field settings in its FieldDefinition stored in the content type. An example of this field setting is shown below and controls if input is validated as ISBN-13 or ISBN-10: ```php [ 'isISBN13' => true, ]; ``` # Keyword field type > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). This field type stores one or several comma-separated keywords as a string or array of strings. | Name | Internal name | Expected input | | --------- | --------------- | ---------------------- | | `Keyword` | `ibexa_keyword` | `string[]` or `string` | ## PHP API field type ### Input expectations | Type | Example | | ---------- | --------------------------------------------------------- | | `string` | `"documentation"` | | `string` | `"php, Ibexa Platform, html5"` | | `string[]` | `[ "Ibexa", "Enterprise", "User Experience Management" ]` | ### Value object #### Properties The Value class of this field type contains the following properties: | Property | Type | Description | | --------- | ---------- | -------------------------------------- | | `$values` | `string[]` | Holds an array of keywords as strings. | ```php // Value object content example use Ibexa\Core\FieldType\Keyword\Value; // Instantiates a Value object $keywordValue = new Value(); // Sets an array of keywords as a value $keywordValue->values = ['php', 'css3', 'html5', 'Ibexa Platform']; ``` #### Constructor The `Keyword\Value` constructor initializes a new value object with the value provided. It expects a list of keywords, either comma-separated in a string or as an array of strings. ```php // Constructor example use Ibexa\Core\FieldType\Keyword\Value; // Instantiates a Value object with an array of keywords $keywordValue = new Value(['php5', 'css3', 'html5']); // Instantiates a Value object with a list of keywords in a string // This is equivalent to the example above $keywordValue = new Value('php5,css3,html5'); ``` # MapLocation field type > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). This field type represents a geographical location. As input it expects three values: - two float values latitude and longitude, - a string value, corresponding to the name or address of the location. | Name | Internal name | Expected input | | ------------- | --------------------- | -------------- | | `MapLocation` | `ibexa_gmap_location` | `mixed` | ## PHP API field type ### Input expectations | Type | Example | | ------- | ------------------------------------------------------------------------------------- | | `array` | `[ 'latitude' => 59.928732, 'longitude' => 10.777888, 'address' => "Ibexa Nordics" ]` | ### Value object #### Properties The Value class of this field type contains the following properties: | Property | Type | Description | | ------------ | -------- | ----------------------------------------------------------------------- | | `$latitude` | `float` | This property stores the latitude value of the map location reference. | | `$longitude` | `float` | This property stores the longitude value of the map location reference. | | `$address` | `string` | This property stores the address of map location. | #### Constructor The `MapLocation\Value` constructor initializes a new value object with values provided as hash. Accepted keys are `latitude` (`float`), `longitude` (`float`), `address` (`string`). ```php // Constructor example use Ibexa\Core\FieldType\MapLocation as MapLocation; // Instantiates a MapLocation Value object $MapLocationValue = new MapLocation\Value( [ 'latitude' => 59.928732, 'longitude' => 10.777888, 'address' => 'Ibexa Nordics', ] ); ``` # Matrix field type > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). This field represents and handles a table of rows and columns of data. | Name | Internal name | Expected input | | -------- | -------------- | -------------- | | `Matrix` | `ibexa_matrix` | `array` | The Matrix field type is available via the Matrix Bundle provided by the [ibexa/fieldtype-matrix](https://github.com/ibexa/fieldtype-matrix) package. ## PHP API field type ### Input expectations | Type | Description | Example | | ------- | -------------------------------------------------------------------------------------- | --------- | | `array` | array of `Ibexa\FieldTypeMatrix\FieldType\Value\Row` objects which contain column data | see below | Example of input: ```php use Ibexa\FieldTypeMatrix\FieldType; new FieldType\Value([ new FieldType\Value\Row(['col1' => 'Row 1, Col 1', 'col2' => 'Row 1, Col 2']), new FieldType\Value\Row(['col1' => 'Row 2, Col 1', 'col2' => 'Row 2, Col 2']), new FieldType\Value\Row(['col1' => 'Row 3, Col 1', 'col2' => 'Row 3, Col 2']), ]); ``` ### Value object `Ibexa\FieldTypeMatrix\FieldType\Value` offers the following properties: | Property | Type | Description | | -------- | ---------------- | ------------------------------------------------------------------------------------------------------------------------- | | `rows` | `RowsCollection` | Array of `Row` objects containing an array of cells (`Row::getCells()` returns array `['col1' => 'Value 1', /* ... */]`). | ### Validation The minimum number of rows is set on content type level for each field. Validation checks for empty rows. A row is considered empty if it contains only empty cells (or cells containing only spaces). Empty rows are removed. If, after removing empty rows, the number of rows doesn't fulfill the configured `Minimum number of rows`, the field doesn't validate. For example, the following input doesn't validate if `Minimum number of rows` is set to 3, because the second row is empty: ```php use Ibexa\FieldTypeMatrix\FieldType; new FieldType\Value([ new FieldType\Value\Row(['col1' => 'Row 1, Col 1', 'col2' => 'Row 1, Col 2']), new FieldType\Value\Row(['col1' => '', 'col2' => '']), new FieldType\Value\Row(['col1' => 'Row 3, Col 1', 'col2' => 'Row 3, Col 2']), ]); ``` ## GraphQL field type operations To get a field of the Matrix field type with GraphQL, you need to specify a content ID, a content type, and a field type. The types that are returned are named after the Type and the field: - `{TypeIdentifier}{FieldIdentifier}Row` The example below shows a GraphQL query for a Recipe content item (belonging to a content type with a Matrix field added), that has two fields: - `name`: `ibexa_string` - `ingredients`: `ibexa_matrix` with two columns: `ingredient` and `quantity` ```graphql { content { recipe(id: 123) { name ingredients { ingredient quantity } } } } ``` The Type returned for the Matrix field exposes columns defined in the field definition: ```json { "data": { "content": { "recipe": { "name": "Cake ingredients", "ingredients": [ { "ingredient": "Butter", "quantity": "200 grams" }, { "ingredient": "Sugar", "quantity": "100 grams" } ] } } } } ``` ### Query for the field type and field definition's details With this query you can inspect details of specific content type. In case of a Matrix field, you can ask for the list of columns, their names, and identifiers. ```graphql { content { _types { recipe { ingredients { settings { minimumRows columns { name identifier } } } } } } } ``` The response lists the exposed field type settings: - minimumRows - columns - name - identifier Example response: ```json { "data": { "content": { "_types": { "recipe": { "ingredients": { "settings": { "minimumRows": 1, "columns": [ { "name": "ingredient", "identifier": "ingredient" }, { "name": "quantity", "identifier": "quantity" } ] } } } } } } } ``` ### Mutation To create a Matrix field type you need to define field type and field definition identifiers. The types that are used for input are named after the Type and the field: - `{TypeIdentifier}{FieldIdentifier}RowInput`, for example, `dish.nutritionFacts`, `event.agenda`: `DishNutritionFactsRowInput`, `EventAgendaRowInput` The example below shows how to create a Recipe content item (belonging to a content type with a Matrix field type added) that has two fields: - `name`: `"Cake Ingredient List"` - `ingredients`: `ibexa_matrix` with two columns: `ingredient` and `quantity` ```graphql mutation AddRecipe { createRecipe( language: eng_GB parentLocationId: 2, input: { name: "Cake Ingredient List", ingredients: [ {ingredient: "sugar", quantity: "100 grams"} {ingredient: "butter", quantity: "200 grams"} ] } ) { name } } ``` The response confirms creation of the new Recipe field: ```json { "data": { "createRecipe": { "name": "Cake Ingredient List" } } } ``` # Measurement field type > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). The Measurement field type represents measurement information. It stores the unit of measure, and either a single measurement value, or a pair of top and bottom values that defines a range. | Name | Internal name | Expected input type | | ------------- | ------------------- | -------------------------------------------------- | | `Measurement` | `ibexa_measurement` | `Ibexa\Contracts\Measurement\Value\ValueInterface` | ## PHP API field type ### Input expectations To create a value, you use a service that implements `Ibexa\Contracts\Measurement\MeasurementServiceInterface`. You must inject the service directly with [dependency injection](https://doc.ibexa.co/en/saas/api/php_api/php_api/#service-container). The service contains the following API endpoints: - `buildSimpleValue` that is used to handle a single value - `buildRangeValue` that is used to handle a range Assuming that the service exists as `$measurementService`, the expected input examples are as follows: | Type | Example | | --------------------------------------------------------- | -------------------------------------------------------------------- | | `\Ibexa\Contracts\Measurement\Value\SimpleValueInterface` | `$measurementService->buildSimpleValue('length', 2.5, 'centimeter')` | | `\Ibexa\Contracts\Measurement\Value\RangeValueInterface` | `$measurementService->buildRangeValue('length', 1.2, 4.5, 'inch')` | ### Value object #### Properties The Value class of this field type contains the following properties: | Property | Type | Description | | -------- | -------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `$value` | `Ibexa\Contracts\Measurement\Value\ValueInterface` | Stores the Measurement API Value, which can be either an instance of `Ibexa\Contracts\Measurement\Value\SimpleValueInterface` or `Ibexa\Contracts\Measurement\Value\RangeValueInterface`. | #### Constructor The `Measurement\Value` constructor for this value object initializes a new value object with the value provided. As its first argument it accepts an object of `Ibexa\Contracts\Measurement\Value\ValueInterface` type. Depending on the selected input type, the object resembles the following examples: ```php // Simple input (single value) example use Ibexa\Measurement\FieldType\MeasurementValue; /** @var \Ibexa\Contracts\Measurement\MeasurementServiceInterface $measurementService */ // Instantiates a Measurement Value object $measurementValue = new MeasurementValue( $measurementService->buildSimpleValue( 'length', 13.5, 'centimeter' ) ); ``` ```php // Range input value example use Ibexa\Measurement\FieldType\MeasurementValue; /** @var \Ibexa\Contracts\Measurement\MeasurementServiceInterface $measurementService */ // Instantiates a Measurement Value object $measurementValue = new MeasurementValue( $measurementService->buildRangeValue( 'volume', 0.5, 0.7, 'liter' ) ); ``` ### Validation The Measurement field type validates measurement types and units passed within the value object against a list of the ones that the system supports, which can be found in the `vendor/ibexa/measurement/src/bundle/Resources/config/builtin_units.yaml` file. ### Modify and add Measurement types and units You can extend the default list of Measurement types and units by modifying the existing entries or adding new ones. To do this, you modify the YAML configuration. To override an existing designation of the unit of measure by changing the symbol that corresponds to a nautical unit of speed, and to add a rotational speed unit, add the following lines to your [YAML configuration](https://doc.ibexa.co/en/saas/administration/configuration/configuration/#configuration-files): ```yaml ibexa_measurement: types: speed: knot: { symbol: kt } revolutions per minute: { symbol: RPM } ibexa: system: default: measurement: types: speed: - revolutions per minute ``` To add a new Measurement type with its own new units, add the following lines to your YAML configuration: ```yaml ibexa_measurement: types: my_type: my_unit: { symbol: my, is_base_unit: true } ibexa: system: default: measurement: types: my_type: - my_unit ``` The configuration also requires that exactly one unit needs to be marked as `is_base_unit` as in highlighted line above. > **Note: Note** > > To be available for selection in the back office, each new Measurement type or unit must be enabled for the back office SiteAccess. Next, you need to define how the new unit should be converted under the `ibexa.system.<scope>.ibexa_measurement` [configuration key](https://doc.ibexa.co/en/saas/administration/configuration/configuration/#configuration-files): ```yaml ibexa_measurement: conversion: formulas: - { source_unit: foo, target_unit: bar, formula: 'value / 100' } types: length: foo: { symbol: foo } bar: { symbol: bar } ``` > **Tip: Tip** > > The `target_unit` must be an existing unit, for example meter, otherwise the conversion results in an error. # Media field type > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). This field type represents and handles a media (audio/video) binary file. It's capable of handling the following types of files: - Apple QuickTime - Adobe Flash - Microsoft Windows Media - Real Media - Silverlight - HTML5 Video - HTML5 Audio | Name | Internal name | Expected input | | ------- | ------------- | -------------- | | `Media` | `ibexa_media` | mixed | ## PHP API field type ### Input expectations | Type | Description | Example | | ---------------------------------- | ---------------------------------------------------------------------------------------- | ----------------------------- | | `string` | Path to the media file. | `/Users/jane/butterflies.mp4` | | `Ibexa\Core\FieldType\Media\Value` | Media field type value object with path to the media file as the value of `id` property. | See below. | ### Value object #### Properties `Ibexa\Core\FieldType\Media\Value` offers the following properties. Both `Media` and `BinaryFile` Value and Type inherit from the `BinaryBase` abstract field type and share common properties. | Property | Type | Description | Example | | --------------- | ------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------- | | `id` | string | Media file identifier. This ID depends on the [IO Handler](https://doc.ibexa.co/en/saas/infrastructure_and_maintenance/clustering/clustering/#dfs-io-handler) that is being used. With the native, default handlers (FileSystem and Legacy), the ID is the file path, relative to the binary file storage root dir (`var/<vardir>/storage/original` by default). | application/63cd472dd7819da7b75e8e2fee507c68.mp4 | | `fileName` | string | The human-readable file name, as exposed to the outside. Used to name the file when sending it for download. | butterflies.mp4 | | `fileSize` | int | File size, in bytes. | 1077923 | | `mimeType` | string | The file's MIME type. | video/mp4 | | `uri` | string | The binary file's HTTP URI. If the URI doesn't include a host or protocol, it applies to the request domain. **The URI is not publicly readable, and must NOT be used to link to the file for download.** Use `ibexa_render_field` to generate a valid link to the download controller. | /var/ezdemo_site/storage/original/application/63cd472dd7819da7b75e8e2fee507c68.mp4 | | `hasController` | boolean | Whether the media has a controller when being displayed. | true | | `autoplay` | boolean | Whether the media should be automatically played. | true | | `loop` | boolean | Whether the media should be played in a loop. | false | | `height` | int | Height of the media. | 300 | | `width` | int | Width of the media. | 400 | | `path` | string | **deprecated** | | ### Hash format The hash format mostly matches the value object. It has the following keys: - `id` - `path` (for backwards compatibility) - `fileName` - `fileSize` - `mimeType` - `uri` - `hasController` - `autoplay` - `loop` - `height` - `width` ### Validation The field type supports `FileSizeValidator`, defining maximum size of media file in bytes: | Name | Type | Default value | Description | | ------------- | ----- | ------------- | ---------------------------------- | | `maxFileSize` | `int` | `false` | Maximum size of the file in bytes. | ```php // Example of using Media field type validator in PHP use Ibexa\Core\FieldType\Media\Type; /** @var \Ibexa\Contracts\Core\Repository\Repository $repository */ $contentTypeService = $repository->getContentTypeService(); $mediaFieldCreateStruct = $contentTypeService->newFieldDefinitionCreateStruct('media', 'ibexa_media'); // Setting maximum file size to 5 megabytes $mediaFieldCreateStruct->validatorConfiguration = [ 'FileSizeValidator' => [ 'maxFileSize' => 5 * 1024 * 1024, ], ]; ``` ### Settings The field type supports the `mediaType` setting, defining how the media file should be handled in output. | Name | Type | Default value | Description | | ----------- | ----- | ------------------------ | ----------------------------------------------------------- | | `mediaType` | mixed | `Type::TYPE_HTML5_VIDEO` | Type of the media, accepts one of the predefined constants. | List of all available `mediaType` constants is defined in the `Ibexa\Core\FieldType\Media\Type` class: | Name | Description | | ------------------- | ----------------------- | | `TYPE_FLASH` | Adobe Flash | | `TYPE_QUICKTIME` | Apple QuickTime | | `TYPE_REALPLAYER` | Real Media | | `TYPE_SILVERLIGHT` | Silverlight | | `TYPE_WINDOWSMEDIA` | Microsoft Windows Media | | `TYPE_HTML5_VIDEO` | HTML5 Video | | `TYPE_HTML5_AUDIO` | HTML5 Audio | ```php // Example of using Media field type settings in PHP use Ibexa\Core\FieldType\Media\Type; /** @var \Ibexa\Contracts\Core\Repository\Repository $repository */ $contentTypeService = $repository->getContentTypeService(); $mediaFieldCreateStruct = $contentTypeService->newFieldDefinitionCreateStruct('media', 'ibexa_media'); // Setting Adobe Flash as the media type $mediaFieldCreateStruct->fieldSettings = [ 'mediaType' => Type::TYPE_FLASH, ]; ``` # Null field type > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). This field type is used as fallback for migration scenarios, and for testing purposes. | Name | Internal name | Expected input type | | ------ | ------------- | ------------------- | | `Null` | (variable) | mixed | ## Description The Null field type aids when migrating from eZ Publish Platform and earlier legacy versions. It's a dummy for legacy field types that aren't implemented in Cohesivo. Null field type accepts anything provided as a value and is usually combined with: - NullConverter: Makes it not store anything to the legacy storage engine (database), nor it reads any data. - Unindexed: Indexable class making sure nothing is indexed to configured search engine. This field type doesn't have its own fixed internal name. Its identifier is instead configured as needed by passing it as an argument to the constructor. ### Example for usage of Null field type The following example shows how an `example` field type could be configured as a Null field type: ```yaml # Null Fieldtype example configuration services: ibexa.field_type.example: class: Ibexa\Core\FieldType\Null\Type arguments: [example] tags: [{name: ibexa.field_type, alias: example}] ibexa.field_type.example.converter: class: Ibexa\Core\Persistence\Legacy\Content\FieldValue\Converter\NullConverter tags: [{name: ibexa.field_type.storage.legacy.converter, alias: example}] ibexa.field_type.example.indexable: class: Ibexa\Core\FieldType\Unindexed tags: [{name: ibexa.field_type.indexable, alias: example}] ``` # Page field type > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). Editions: Experience Page field type represents a page with a layout consisting of multiple zones. Each zone can in turn contain blocks. Page field type is only used in the page content type that is included in Ibexa Experience. | Name | Internal name | Expected input | | ------------- | -------------------- | --------------- | | `LandingPage` | `ibexa_landing_page` | `string` (JSON) | > **Caution: Page Builder** > > If you create content type with both `ibexa_landing_page` and `ibexa_user` field types, you aren't redirected to Page Builder after selecting `Edit` or `Create`. This is caused by `ibexa_user` field type which requires separate handling. You're redirected to the standard back office edit or create mode. ## Layout and zones Layout defines how a page is divided into zones. The placement of zones is defined in a template which is a part of the layout configuration. You can modify the template to define your own zone layout. For information on how to create and configure new blocks for the page, see [Page layouts](https://doc.ibexa.co/en/saas/templating/render_content/render_page/#render-a-layout). ## Blocks For information on how to create and configure new blocks for the page, see [Create custom Page block](https://doc.ibexa.co/en/saas/content_management/pages/create_custom_page_block/index.md). ## Rendering pages Page rendering takes place while editing or viewing. When rendering a page, its zones are passed to the layout as a `zones` array with a `blocks` array each. You can access them using twig (for example, `{{ zones[0].id }}` ). Each div that's a zone should have the `data-ibexa-zone-id` attribute with zone ID as a value for a zone container. To render a block inside the layout, use the Twig [`render_esi()`](https://symfony.com/doc/7.4/reference/twig_reference.html#render-esi) function to call `Ibexa\\Bundle\\FieldTypePage\\Controller\\BlockController::renderAction`. The `renderAction` has the following parameters: | Parameter | Description | | -------------- | -------------------------------------------------------------------------------- | | `locationId` | ID of the location of the content item which can be accessed by `contentInfo.id` | | `blockId` | ID of the block which you want to render. | | `versionNo` | Version number of the content item to render. | | `languageCode` | Language code of the content item to render. | If your block needs to be dependent on query parameters like "page" and you already configured your custom block with a [`cacheable_query_params configuration`](https://doc.ibexa.co/en/saas/content_management/pages/page_blocks/#block-configuration), pass `ibexa_append_cacheable_query_params(block)` as the third argument to the [`controller()` Twig function](https://symfony.com/doc/7.4/reference/twig_reference.html#controller) so that the HTTP cache can vary based on those query parameters. In a fresh installation, the feature is only used by the back office's [Dashboard blocks](https://doc.ibexa.co/projects/userguide/en/6.0/getting_started/dashboard/dashboard_block_reference/): "My content" and "Review queue". Example usage: ```html+twig {{ render_esi(controller('Ibexa\\Bundle\\FieldTypePage\\Controller\\BlockController::renderAction', { 'locationId': locationId, 'blockId': block.id, 'versionNo': versionInfo.versionNo, 'languageCode': field.languageCode }, ibexa_append_cacheable_query_params(block))) }} ``` As a whole a sample layout could look as follows: ```html+twig <div> {# The required attribute for the displayed zone #} <div data-ibexa-zone-id="{{ zones[0].id }}"> {# If a zone with [0] index contains any blocks #} {% if zones[0].blocks %} {# for each block #} {% for block in blocks %} {# create a new layer with appropriate ID #} <div class="landing-page__block block_{{ block.type }}" data-ibexa-block-id="{{ block.id }}"> {# render the block by using the "Ibexa\\Bundle\\FieldTypePage\\Controller\\BlockController::renderAction" controller #} {# location.id is the ID of the Location of the current content item, block.id is the ID of the current block #} {{ render_esi(controller('Ibexa\\Bundle\\FieldTypePage\\Controller\\BlockController::renderAction', { 'locationId': locationId, 'blockId': block.id, 'versionNo': versionInfo.versionNo, 'languageCode': field.languageCode }, ibexa_append_cacheable_query_params(block))) }} </div> {% endfor %} {% endif %} </div> </div> ``` # Product specification field type > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). Editions: Headless This field represents and handles [product attributes](https://doc.ibexa.co/en/saas/product_catalog/products/#product-attributes) and [VAT](https://doc.ibexa.co/en/saas/product_catalog/prices/#vat). Consider it as internal to the [product catalog](https://doc.ibexa.co/en/saas/product_catalog/product_catalog/index.md). | Name | Internal name | Expected input | | ---------------------- | ----------------------------- | -------------- | | `ProductSpecification` | `ibexa_product_specification` | mixed | > **Caution: Caution** > > The presence of a specification (`ibexa_product_specification`) field distincts product types from content types. Don't remove this field from a product type (or it becomes a unreachable hidden content type). Don't add such field to a content type (or it becomes an uneditable unusable product type). # Relation field type > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). This field type makes it possible to store and retrieve the value of a relation to another content item. | Name | Internal name | Expected input | | ---------- | ----------------------- | -------------- | | `Relation` | `ibexa_object_relation` | mixed | ## PHP API field type ### Input expectations | Type | Example | | --------- | ------- | | `string` | `"150"` | | `integer` | `150` | ### Value object #### Properties The Value class of this field type contains the following properties: | Property | Type | Description | | ----------------------- | -------------------------- | ---------------------------------------------------------------------------------------- | | `$destinationContentId` | `string`, `int`, or `null` | This property is used to store the value provided, which represents the related content. | ```php /** * Value object content example. * * @var \Ibexa\Core\FieldType\Relation\Value $relation * @var \Ibexa\Contracts\Core\Repository\Values\Content\ContentInfo $contentInfo */ $relation->destinationContentId = $contentInfo->id; ``` #### Constructor The `Relation\Value` constructor initializes a new value object with the value provided. It expects a mixed value. ```php // Constructor example use Ibexa\Core\FieldType\Relation as Relation; /** @var \Ibexa\Contracts\Core\Repository\Values\Content\ContentInfo $contentInfo */ // Instantiates a Relation Value object $relationValue = new Relation\Value($contentInfo->id); ``` ### Validation This field type validates whether the provided relation exists, but before that it checks that the value is either a string or an int. ### Settings The field definition of this field type can be configured with three options: | Name | Type | Default value | Description | | ----------------------- | -------- | --------------------------------- | ------------------------------------------------------------------------------ | | `selectionMethod` | `int` | `Relation\Type::SELECTION_BROWSE` | *This setting is not implemented yet, only one selection method is available.* | | `selectionRoot` | `string` | `null` | This setting defines the selection root. | | `selectionContentTypes` | `array` | `[]` | An array of content type IDs that are allowed for related Content. | ```php // Relation FieldType example settings use Ibexa\Core\FieldType\Relation\Type; $settings = [ 'selectionMethod' => 1, 'selectionRoot' => null, 'selectionContentTypes' => [], ]; ``` # RelationList field type > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). This field type makes it possible to store and retrieve values of a relation to other content items. | Name | Internal name | Expected input | | -------------- | ---------------------------- | -------------- | | `RelationList` | `ibexa_object_relation_list` | `mixed` | ## PHP API field type ### Input expectations | Type | Description | Example | | ------------------------------------------------------------ | ------------------------------------------- | ------------ | | `int` or `string` | ID of the related content item | `42` | | `array` | An array of related Content IDs | `[ 24, 42 ]` | | `Ibexa\Contracts\Core\Repository\Values\Content\ContentInfo` | ContentInfo instance of the related Content | n/a | | `Ibexa\Core\FieldType\RelationList\Value` | RelationList field type value object | See below. | ### Value Object #### Properties `Ibexa\Core\FieldType\RelationList\Value` contains the following properties: | Property | Type | Description | Example | | ----------------------- | ------- | ------------------------------- | ------------ | | `destinationContentIds` | `array` | An array of related Content IDs | `[ 24, 42 ]` | ```php /** * Value object content example. * * @var \Ibexa\Core\FieldType\RelationList\Value $relationList * @var \Ibexa\Contracts\Core\Repository\Values\Content\ContentInfo $contentInfo1 * @var \Ibexa\Contracts\Core\Repository\Values\Content\ContentInfo $contentInfo2 */ $relationList->destinationContentIds = [ $contentInfo1->id, $contentInfo2->id, 170, ]; ``` #### Constructor The `RelationList\Value` constructor initializes a new value object with the value provided. It expects a mixed array as value. ```php //Constructor example use Ibexa\Core\FieldType\RelationList as RelationList; /** * @var \Ibexa\Contracts\Core\Repository\Values\Content\ContentInfo $contentInfo1 * @var \Ibexa\Contracts\Core\Repository\Values\Content\ContentInfo $contentInfo2 */ // Instantiates a RelationList Value object $relationListValue = new RelationList\Value( [ $contentInfo1->id, $contentInfo2->id, 170, ] ); ``` ### Validation This field type validates if: - the `selectionMethod` specified is `\Ibexa\Core\FieldType\RelationList\Type::SELECTION_BROWSE` or `\Ibexa\Core\FieldType\RelationList\Type::SELECTION_DROPDOWN`. A validation error is thrown if the value doesn't match. - the `selectionDefaultLocation` specified is `null`, `string` or `integer`. If the type validation fails a validation error is thrown. - the value specified in `selectionContentTypes` is an `array`. If not, a validation error in given. - the number of content items selected in the field isn't greater than the `selectionLimit`. > **Note: Note** > > The dropdown selection method isn't implemented yet. ### Settings The field definition of this field type can be configured with the following options: | Name | Type | Default value | Description | | -------------------------- | --------------------- | ------------------ | ------------------------------------------------------------------------------- | | `selectionMethod` | `mixed` | `SELECTION_BROWSE` | Method of selection in the back-end interface. | | `selectionDefaultLocation` | `string` or `integer` | `null` | ID of the default Location for the selection when using the back-end interface. | | `selectionContentTypes` | `array` | `[]` | An array of content type IDs that are allowed for related Content. | Following selection methods are available: | Name | Description | | -------------------- | --------------------------- | | `SELECTION_BROWSE` | Selection uses browse mode. | | `SELECTION_DROPDOWN` | *Not implemented yet* | ### Validators | Name | Type | Default value | Description | | -------------------------------------------- | --------- | ------------- | --------------------------------------------------------------------------------------------------------- | | `RelationListValueValidator[selectionLimit]` | `integer` | `0` | The number of content items that can be selected in the field. When set to 0, any number can be selected. | ```php // Example of using settings and validators configuration in PHP use Ibexa\Core\FieldType\RelationList\Type; $fieldSettings = [ 'selectionMethod' => Type::SELECTION_BROWSE, 'selectionDefaultLocation' => null, 'selectionContentTypes' => [], ]; $validators = [ 'RelationListValueValidator' => [ 'selectionLimit' => 0, ], ]; ``` ### GraphQL integration This field type is paginating the results when queried using [GraphQL](https://doc.ibexa.co/en/saas/api/graphql/graphql/index.md). To learn more, see [Pagination in GraphQL](https://doc.ibexa.co/en/saas/api/graphql/graphql_queries/#pagination). # RichText field type > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). The RichText field type is available via the RichText field type Bundle provided by the [ibexa/fieldtype-richtext](https://github.com/ibexa/fieldtype-richtext) package. This field type validates and stores structured rich text in [DocBook](https://docbook.org/) XML format, and exposes it in several formats. | Name | Internal name | Expected input | | ---------- | ---------------- | -------------- | | `RichText` | `ibexa_richtext` | mixed | ## PHP API field type ### Value object `Ibexa\FieldTypeRichText\FieldType\RichText\Value` offers the following properties: | Property | Type | Description | | -------- | ------------- | ------------------------------------------------------ | | `xml` | `DOMDocument` | Internal format value as an instance of `DOMDocument`. | ### Input expectations | Type | Description | | -------------------------------------------------- | -------------------------------------------------------------------------------- | | `string` | XML document in one of the field type's input formats as a string. | | `DOMDocument` | XML document in one of the field type's input formats as a `DOMDocument` object. | | `Ibexa\FieldTypeRichText\FieldType\RichText\Value` | An instance of the field type's `Value` object. | ### Input formats The field type expects an XML value as input, in the form of a string, `DOMDocument` object, or field type's `Value` object. The field type's `Value` object must hold the value in the field type's [internal format](#internal-format). For a string of a `DOMDocument` object, if the input doesn't conform to this format, it's converted into it. #### Internal format As its internal format, the RichText field type uses a [custom flavor of the DocBook format](#custom-docbook-format). ```xml <?xml version="1.0" encoding="UTF-8"?> <section xmlns="http://docbook.org/ns/docbook" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:ezxhtml="http://ibexa.co/xmlns/dxp/docbook/xhtml" xmlns:ezcustom="http://ibexa.co/xmlns/dxp/docbook/custom" version="5.0-variant ezpublish-1.0"> <title ezxhtml:level="2">This is a title. This is a paragraph.
    ``` #### XHTML5 edit format The XHTML5 format is used by the Online Editor. ```xml

    This is a title.

    This is a paragraph.

    ``` ## Custom DocBook format > **Caution: Caution** > > The custom DocBook format described below is subject to change and isn't covered by backwards compatibility promise. You can use the Ibexa flavor of the DocBook format in PHP API and in REST API requests by providing the DocBook content as a string. The following example shows how to pass DocBook content to a [create struct](https://doc.ibexa.co/en/saas/content_management/content_api/creating_content/#creating-content-item-draft): ```php /** * @var \Ibexa\Contracts\Core\Repository\ContentService $contentService * @var \Ibexa\Contracts\Core\Repository\Values\ContentType\ContentType $contentType */ $contentCreateStruct = $contentService->newContentCreateStruct($contentType, 'eng-GB'); $inputString = <<
    This is a title. This is a paragraph.
    DOCBOOK; $contentCreateStruct->setField('description', $inputString); ``` When creating RichText content with the REST API, use the `xml` key of the `fieldValue` tag: ```xml <?xml version="1.0" encoding="UTF-8"?> <section xmlns="http://docbook.org/ns/docbook" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:ezxhtml="http://ibexa.co/xmlns/dxp/docbook/xhtml" xmlns:ezcustom="http://ibexa.co/xmlns/dxp/docbook/custom" version="5.0-variant ezpublish-1.0"> <title ezxhtml:level="2">This is a title.</title> </section> ``` ### DocBook elements The RichText format enriches [DocBook](https://docbook.org/) with the following custom elements: - `section` - main element of a RichText field - `ezembed` - holds embedded images - `ezembedinline` - holds embedded content items - `eztemplate` - holds custom tags, including built-in custom tags for embedded Facebook, Twitter, and YouTube content - `eztemplateinline` - holds inline custom tags - `ezconfig` - contains configuration for custom tags and other elements - `ezvalue` - contains values for other elements, such as `ezconfig` or `ezembed` - `ezattribute` - contains attributes for other elements, such as `ezconfig` or `ezembed` > **Note: Unsupported DocBook elements** > > Some DocBook elements aren't supported by RichText. Refer to [`ezpublish.rng`](https://github.com/ibexa/fieldtype-richtext/blob/6.0/src/bundle/Resources/richtext/schemas/docbook/ezpublish.rng#L137) for a full list. ### Online Editor elements Elements of the Online Editor correspond to the following sample DocBook code blocks. #### Text formatting ```xml Anchor text Center aligned Left aligned bold italic underlined subscript superscript crossed out
    This is a block quote.
    ``` #### Heading ```xml My heading ``` #### Code block ```xml ``` #### Unordered list ```xml 1st level bullet point 1st level bullet point 2nd level bullet point 2nd level bullet point ``` #### Ordered list ```xml 1st level numbered point 1st level numbered point 2nd level numbered point ``` #### Embedded content ```xml ``` #### Inline embedded content ```xml embed inline ``` #### Image ```xml medium ``` #### Table ```xml This is a merged table cell ``` #### YouTube ```xml https://youtu.be/Y-1d5zdeg9A false ``` #### Twitter ```xml https://twitter.com/BBCSpringwatch/status/1401622026973032452 light 500 en true ``` #### Facebook ```xml https://www.facebook.com/bbcnews/posts/10158930827817217?__tn__=-R 120 ``` # Selection field type > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). The Selection field type stores single selections or multiple choices from a list of options, by populating a hash with the list of selected values. | Name | Internal name | Expected input type | | ----------- | ----------------- | ------------------- | | `Selection` | `ibexa_selection` | mixed | ## PHP API field type ### Input expectations | Type | Example | | ------- | ---------- | | `array` | `[ 1, 2 ]` | ### Value object #### Properties The Value class of this field type contains the following properties: | Property | Type | Description | | ------------ | ------- | ----------------------------------------------------------------------------------------------------------------- | | `$selection` | `int[]` | This property is used for the list of selections, which is a list of integer values, or one single integer value. | ```php // Value object content examples /** @var \Ibexa\Core\FieldType\Selection\Value $value */ // Single selection $value->selection = [1]; // Multiple selection $value->selection = [1, 4, 5]; ``` #### Constructor The `Selection\Value` constructor accepts an array of selected element identifiers. ```php // Constructor example use Ibexa\Core\FieldType\Selection as Selection; // Instanciates a selection value with items #1 and #2 selected $selectionValue = new Selection\Value([1, 2]); ``` #### String representation String representation of this field type is its list of selections as a string, concatenated with a comma. Example: `"1,2,24,42"` #### Hash format Hash format of this field type is the same as value object's `selection` property. ```php // Example of value in hash format $hash = [1, 2]; ``` ### Validation This field type validates the input, verifying if all selected options exist in the field definition and checks if multiple selections are allowed in the field definition. If any of these validations fail, a `ValidationError` is thrown, specifying the error message. When option validation fails, a list with the invalid options is also presented. ### Settings | Name | Type | Default value | Description | | ------------ | --------- | ------------- | ------------------------------------------------------------------ | | `isMultiple` | `boolean` | `false` | Used to allow or prohibit multiple selection from the option list. | | `options` | `hash` | `[]` | Stores the list of options defined in the field definition. | ```php // Selection field type example settings use Ibexa\Core\FieldType\Selection\Type; $settings = [ 'isMultiple' => true, 'options' => [1 => 'One', 2 => 'Two', 3 => 'Three'], ]; ``` # TaxonomyEntry field type > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). TaxonomyEntry is a field type that stores information about the parent entry in the taxonomy tree, placing the taxonomy entry (tag or product category) in the taxonomy structure. | Name | Internal name | Expected input | | --------------- | ---------------------- | -------------- | | `TaxonomyEntry` | `ibexa_taxonomy_entry` | `array` | ## PHP API field type ### Input expectations A `TaxonomyEntry` field accepts an array with an `Ibexa\Contracts\Taxonomy\Value\TaxonomyEntry` object. | Type | Description | Example | | ------- | -------------------------------------------------------------------------------------------------- | --------- | | `array` | array with an `Ibexa\Contracts\Taxonomy\Value\TaxonomyEntry` object under the `taxonomy_entry` key | see below | Example using an `Ibexa\Taxonomy\FieldType\TaxonomyEntry\Value` object: ```php use Ibexa\Contracts\Taxonomy\Service\TaxonomyServiceInterface; /** @var TaxonomyServiceInterface $taxonomyService */ $taxonomyEntry = $taxonomyService->loadEntryByIdentifier('example_entry', 'tags'); $taxonomyEntryField = new \Ibexa\Taxonomy\FieldType\TaxonomyEntry\Value($taxonomyEntry); ``` Example using array: ```php use Ibexa\Contracts\Taxonomy\Value\TaxonomyEntry; /** @var TaxonomyEntry $taxonomyEntry */ return [ 'taxonomy_entry' => $taxonomyEntry, // load Entry using TaxonomyService ]; ``` ### Value object #### Properties | Property | Type | Description | | --------------- | ----------------------------------------------- | ------------------------------- | | `taxonomyEntry` | `?Ibexa\Contracts\Taxonomy\Value\TaxonomyEntry` | Stores selected taxonomy entry. | #### Constructor The constructor accepts an `Ibexa\Contracts\Taxonomy\Value\TaxonomyEntry` object. ```php // Constructor example use Ibexa\Contracts\Taxonomy\Service\TaxonomyServiceInterface; use Ibexa\Taxonomy\FieldType\TaxonomyEntry; // Fetches TaxonomyEntry from TaxonomyService /** @var TaxonomyServiceInterface $taxonomyService */ $taxonomyEntry = $taxonomyService->loadEntryByIdentifier('example_entry', 'tags'); // Instantiates a taxonomy entry value $taxonomyEntryFieldTypeValue = new TaxonomyEntry\Value($taxonomyEntry); ``` #### String representation `taxonomyEntry` string identifier or empty string if no Taxonomy Entry is selected. #### Hash format An array with `taxonomy_entry` key containing `Ibexa\Contracts\Taxonomy\Value\TaxonomyEntry` object or `null`. #### Validation No validation. #### Settings The field definition of this field type can be configured with the following options: | Name | Type | Default value | Description | | ---------- | -------- | ------------- | ---------------------------------------- | | `taxonomy` | `string` | `null` | Taxonomy from which you choose an entry. | # TaxonomyEntryAssignment field type > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). `TaxonomyEntryAssignment` field is used to integrate content with the Taxonomy module. It allows you to select tags or categories and assign them to content. This field type assigns tags to the content in the data action, so then you can use `TaxonomyService` on this content item. > **Caution: Duplicate taxonomy fields** > > Because tags are assigned per content item, not per field, you cannot use two **Taxonomy Entry Assignment** fields with the same taxonomy type in one content type. To be able to assign tags to the content, first, you need to add a `TaxonomyEntryAssignment` field to the content type definition. | Name | Internal name | Expected input | | ------------------------- | --------------------------------- | ------------------------------------------------ | | `TaxonomyEntryAssignment` | `ibexa_taxonomy_entry_assignment` | array with `taxonomyEntries` and `taxonomy` keys | ## PHP API field type ### Input expectations | Type | Description | Example | | ------- | ------------------------------------------------------------------------------------------------------------------------------------------- | --------- | | `array` | array with `Ibexa\Contracts\Taxonomy\Value\TaxonomyEntry` objects under `taxonomy_entries` key and Taxonomy identifier under `taxonomy` key | see below | Example using an `Ibexa\Taxonomy\FieldType\TaxonomyEntryAssignment\Value` object: ```php use Ibexa\Contracts\Taxonomy\Service\TaxonomyServiceInterface; /** @var TaxonomyServiceInterface $taxonomyService */ $taxonomyEntry1 = $taxonomyService->loadEntryByIdentifier('example_entry', 'tags'); $taxonomyEntry2 = $taxonomyService->loadEntryByIdentifier('example_entry_2', 'tags'); new \Ibexa\Taxonomy\FieldType\TaxonomyEntryAssignment\Value( [ $taxonomyEntry1, $taxonomyEntry2, // ... ], 'tags', ); ``` Example using array: ```php use Ibexa\Contracts\Taxonomy\Value\TaxonomyEntry; /** * @var TaxonomyEntry $taxonomyEntry * @var TaxonomyEntry $taxonomyEntry2 */ return [ 'taxonomy_entries' => [$taxonomyEntry, $taxonomyEntry2], // load entries using TaxonomyService 'taxonomy' => 'tags', ]; ``` ### Value object #### Properties | Property | Type | Description | | --------------- | ------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `taxonomyEntry` | array of `Ibexa\Contracts\Taxonomy\Value\TaxonomyEntry` | Stores selected taxonomy entry. | | `taxonomy` | `string` | Stores the taxonomy identifier, all `taxonomyEntries` have to be assigned to this taxonomy and the identifier has to match the settings of the field type in content type configuration. | #### Constructor The constructor accepts `taxonomyEntries` and `taxonomy` as described above. #### String representation If the field has no entries - empty string. If the field has entries (for example: "Cars and 5 more") - a string displaying the first taxonomy entry and the number of rest of the entries. #### Hash format An array of: - `taxonomy_entries` with numerical IDs of entries. - `taxonomy` string identifier of a taxonomy. #### Validation The field type validates if all Taxonomy Entries from the value are assigned to the configured taxonomy. #### Settings | Name | Type | Default value | Description | | ---------- | -------- | ------------- | ------------------------------------ | | `taxonomy` | `string` | `null` | Taxonomy from which entry is chosen. | # TextBlock field type > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). The field type handles a block of multiple lines of unformatted text. It's capable of handling up to 16,777,216 characters. | Name | Internal name | Expected input type | | ----------- | ------------- | ------------------- | | `TextBlock` | `ibexa_text` | `string` | ## PHP API field type ### Input expectations | Type | Example | | -------- | --------------------------------------- | | `string` | `"This is a block of unformatted text"` | ### Value object #### Properties The Value class of this field type contains the following properties: | Property | Type | Description | | -------- | -------- | ------------------------------------------- | | `$text` | `string` | This property is used for the text content. | ##### String representation A TextBlock's string representation is the `$text` property's value, as a string. ##### Constructor The constructor for this value object initializes a new value object with the value provided. It accepts a string as argument and imports it to the `$text` attribute. ### Validation This field type doesn't perform any special validation of the input value. ### Settings Settings contain only one option: | Name | Type | Default value | Description | | ---------- | --------- | ------------- | ------------------------------------------------------------- | | `textRows` | `integer` | `10` | Number of rows for the editing box in the back-end interface. | # TextLine field type > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). This field type makes possible to store and retrieve a single line of unformatted text. It's capable of handling up to 255 characters. | Name | Internal name | Expected input type | | ---------- | -------------- | ------------------- | | `TextLine` | `ibexa_string` | `string` | ## PHP API field type ### Value object #### Properties The Value class of this field type contains the following properties: | Property | Type | Description | | -------- | -------- | ------------------------------------------- | | `$text` | `string` | This property is used for the text content. | ##### String representation A TextLine's string representation is the `$text` property's value, as a string. ##### Constructor The constructor for this value object initializes a new value object with the value provided. It accepts a string as argument and imports it to the `$text` attribute. ### Validation The input passed into this field type is subject to validation by the `StringLengthValidator`. The length of the string provided must be between the minimum length defined in `minStringLength` and the maximum defined in `maxStringLength`. The default value for both properties is 0, which means that the validation is disabled by default. To set the validation properties, the `validateValidatorConfiguration()` method needs to be inspected, which receives an array with `minStringLength` and `maxStringLength` like in the following representation: ```php [ 'StringLengthValidator' => [ 'maxStringLength' => 60, 'minStringLength' => 1, ], ]; ``` # Time field type > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). This field type represents time information. Date information is **not stored**. What is stored is the number of seconds, calculated from the beginning of the day in the given or the environment timezone. | Name | Internal name | Expected input type | | ------ | ------------- | ------------------- | | `Time` | `ibexa_time` | mixed | ## PHP API field type ### Input expectations If input value is of type `string` or `integer`, it's passed directly to the [PHP's built-in `\DateTime` class](https://www.php.net/manual/en/datetime.construct.php) constructor, therefore the same input format expectations apply. It's also possible to directly pass an instance of `\DateTime`. | Type | Example | | ----------- | ---------------------------------- | | `string` | `"2012-08-28 12:20 Europe/Berlin"` | | `integer` | `1346149200` | | `\DateTime` | `new \DateTime()` | ### Value object #### Properties The Value class of this field type contains the following properties: | Property | Type | Description | | -------- | ------------------- | --------------------------------------------------------------------------------- | | `$time` | `integer` or `null` | Holds the time information as a number of seconds since the beginning of the day. | #### Constructor The constructor for this value object initializes a new value object with the value provided. It accepts an integer representing the number of seconds since the beginning of the day. #### String representation String representation of the date value generates the date string in the format "H:i:s" as accepted by [PHP's built-in `date()` function](https://www.php.net/manual/en/function.date.php). | Character | Description | Example | | --------- | ------------------------------------------------------------------- | ------- | | H | Two digit representation of an hour, 24-hour format, range 00 to 23 | 12 | | i | Two digit representation of minutes, range 00 to 59 | 14 | | s | Two digit representation of seconds, range 00 to 59 | 56 | Example: `"12:14:56"` #### Hash format Value in hash format is an integer representing a number of seconds since the beginning of the day. Example: `36000` ### Validation This field type doesn't perform validation of the input value. ### Settings The Field definition of this field type can be configured with several options: | Name | Type | Default value | Description | | ------------- | ------------------------------------------------ | --------------------- | --------------------------------------------------------------------------------- | | `useSeconds` | `boolean` | `false` | Used to control displaying of seconds in the output. | | `defaultType` | `Type::DEFAULT_EMPTY Type::DEFAULT_CURRENT_TIME` | `Type::DEFAULT_EMPTY` | The constant used here defines default input value when using back-end interface. | ```php // Time field type example settings use Ibexa\Core\FieldType\Time\Type; $settings = [ 'defaultType' => Type::DEFAULT_EMPTY, ]; ``` # URL field type > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). This field type makes it possible to store and retrieve a URL. It's formed by the combination of a link and the respective text. | Name | Internal name | Expected input | | ----- | ------------- | -------------- | | `Url` | `ibexa_url` | `string` | ## PHP API field type ### Input expectations | Type | Description | Example | | -------- | --------------------------------------------- | ------------------------ | | `string` | Link content provided to the value. | "" | | `string` | Text content that represents the stored link. | "Ibexa" | ### Value object #### Properties The Value class of this field type contains the following properties: | Property | Type | Description | | -------- | -------- | ---------------------------------------------------------------------------------------------------- | | `$link` | `string` | This property stores the link provided to the value of this field type. | | `$text` | `string` | This property stores the text to represent the stored link provided to the value of this field type. | ```php // Value object content example /** @var \Ibexa\Core\FieldType\Url\Value $url */ $url->link = 'https://www.ibexa.co'; $url->text = 'Ibexa'; ``` #### Constructor The `Url\Value` constructor initializes a new value object with the provided value. It expects two comma-separated strings, corresponding to the link and text. ```php // Constructor example use Ibexa\Core\FieldType\Url; // Instantiates an Url Value object $UrlValue = new Url\Value('https://www.ibexa.co/', 'Ibexa'); ``` ### Hash format | Key | Type | Description | Example | | ------ | -------- | ------------- | ------------------------- | | `link` | `string` | Link content. | "" | | `text` | `string` | Text content. | "Ibexa" | ```php // Example of the hash value in PHP $hash = [ 'link' => 'https://www.ibexa.co/', 'text' => 'Ibexa', ]; ``` ### Validation This field type doesn't perform validation. But some validation can be made afterward, see [External URL validation](https://doc.ibexa.co/en/saas/content_management/url_management/url_management/#external-url-validation) for more information. ### Settings This field type doesn't have settings. # User field type > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). This field type validates and stores information about a user. | Name | Internal name | Expected input | | ------ | ------------- | -------------- | | `User` | `ibexa_user` | ignored | ## PHP API field type ### Value object | Property | Type | Description | Example | | ------------------ | ----------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ | --------------------------------- | | `hasStoredLogin` | `boolean` | Denotes if user has stored login. | `true` | | `contentId` | `int` or `string` | ID of the content item corresponding to the user. | `42` | | `login` | `string` | Username. | `john` | | `email` | `string` | The user's email address. | `john@smith.com` | | `passwordHash` | `string` | Hash of the user's password. | `1234567890abcdef` | | `passwordHashType` | `mixed` | Algorithm user for generating password hash as a `PASSWORD_HASH_*` constant defined in `Ibexa\Contracts\Core\Repository\Values\User\User` class. | `User::PASSWORD_HASH_PHP_DEFAULT` | | `maxLogin` | `int` | Maximum number of concurrent logins. | `1000` | #### Available password hash types | Constant | Description | | ----------------------------------------------------------------------------- | ------------------------------------------------------------------------- | | `Ibexa\Contracts\Core\Repository\Values\User\User::DEFAULT_PASSWORD_HASH` | Default password hash, used when none is specified, may change over time. | | `Ibexa\Contracts\Core\Repository\Values\User\User::PASSWORD_HASH_PHP_DEFAULT` | Passwords hashed by PHP's default algorithm, which may change over time. | | `Ibexa\Contracts\Core\Repository\Values\User\User::PASSWORD_HASH_BCRYPT` | Bcrypt hash of the password. | # Templating # Templating > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). To create a website front for your site, you can use the Twig-based templating system. You configure the templates to use by applying content view configuration that covers for different types of content and different parts of your website. To create a website front for your site, you can use the Twig-based templating system. You configure the templates to use by applying content view configuration that covers for different types of content and different parts of your website. The design engine lets you prepare differing template themes. Content view configuration is SiteAccess-aware. - [Templates](https://ez-systems-developer-documentation--3381.com.readthedocs.build/en/3381/templating/templates/templates/): Cohesivo uses the Twig template engine to customize the rendering of content in the site. - [Content queries](https://ez-systems-developer-documentation--3381.com.readthedocs.build/en/3381/templating/queries_and_controllers/content_queries/): Query content by using Query types and content query field. - [Design engine](https://ez-systems-developer-documentation--3381.com.readthedocs.build/en/3381/templating/design_engine/design_engine/): Design engine allows you to use different SiteAccess-aware themes in your site. # Render content > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). Customize rendering of content items on the site front end by using templates with proper content view configuration. Content is rendered automatically by using default, basic templates. To render content with a custom template, you create a template file and inform the system, through configuration, when to use this template. You do it by using the [content view configuration](https://doc.ibexa.co/en/saas/templating/templates/template_configuration/index.md). For example, to apply a custom template to all articles, add the following [configuration](https://doc.ibexa.co/en/saas/administration/configuration/configuration/#configuration-files): ```yaml ibexa: system: site_group: content_view: full: article: template: '@ibexadesign/full/article.html.twig' match: Identifier\ContentType: article ``` This configuration defines a `full` view for all content items that fulfill the conditions in `match`. `match` indicates that all content items with the content type `article` should use this configuration. The indicated `template` is `@ibexadesign/full/article.html.twig`. > **Tip: Designs** > > This configuration uses the [design engine](https://doc.ibexa.co/en/saas/templating/design_engine/design_engine/index.md), as indicated by the `@ibexadesign` in the template path. In this example, the theme used by the design is `my_theme`. > > Using the design engine is recommended, but you can also set direct paths to templates, for example: > > ```yaml > template: 'full/article.html.twig' > ``` > > You must then ensure that the `templates/full` folder contains the template file. The configuration requires that you add the `article.html.twig` template file to `templates/themes//full`, in this example, `templates/themes/my_theme/full`. ```html+twig

    {{ ibexa_content_name(content) }}

    {{ content.contentInfo.publishedDate|ibexa_full_datetime }} {{ ibexa_render_field(content, 'intro') }} {{ ibexa_render_field(content, 'body', { 'attr': { class: 'article-body' } }) }} {{ ibexa_render_field(content, 'author', { 'template': '@ibexadesign/fields/author.html.twig' }) }} ``` ## Get content information To render general content information, such as content name, use the `ibexa_content_name()` Twig function. Content name is based on the [content name pattern](https://doc.ibexa.co/en/saas/administration/content_organization/content_types/#content-type-metadata) of the content type. ```html+twig

    {{ ibexa_content_name(content) }}

    ``` You can get general information about the content, location and view parameters by using the [available variables](https://doc.ibexa.co/en/saas/templating/templates/templates/#template-variables). For example, to get the publication date of the current content item, use: ```html+twig {{ content.contentInfo.publishedDate|ibexa_full_datetime }} ``` > **Tip: Tip** > > For development purposes, you can list all available variables, or a single variable, and their values, by using the `dump()` Twig function: > > ```html+twig > {{ dump() }} > {{ dump(content) }} > ``` ## Render fields You can render a single field of a content item by using the `ibexa_render_field()` Twig function. It takes the content item and the identifier of the Field as arguments: ```html+twig {{ ibexa_render_field(content, 'intro') }} ``` You can pass additional arguments to this function, for example, an HTML class: ```html+twig {{ ibexa_render_field(content, 'body', { 'attr': { class: 'article-body' } }) }} ``` ### Field templates You can use a custom Field template by passing the template as an argument to `ibexa_render_field()`: ```html+twig {{ ibexa_render_field(content, 'author', { 'template': '@ibexadesign/fields/author.html.twig' }) }} ``` In this case you must place the `author.html.twig` template in `templates/themes//fields`, for example `templates/themes/my_theme/fields`. ```html+twig {% block ibexa_author_field %} {% if field.value.authors|length() > 0 %} {% for author in field.value.authors %} {{ author.name }} {% endfor %} {% endif %} {% endblock %} ``` The field template must be placed in a block that corresponds to the field type identifier, in this case `{% block ezauthor_field %}`. > **Tip: Template blocks** > > Twig blocks are used to include templates in one another. For more information about relationships between templates, see [Connecting templates](https://doc.ibexa.co/en/saas/templating/templates/templates/#connecting-templates). # Render a page > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). Prepare templates for page layouts and render page blocks. Editions: Experience Page is a special content type that contains a [page field](https://doc.ibexa.co/en/saas/content_management/field_types/field_type_reference/pagefield/index.md). A page field is a layout composed of zones. Each zone can contain multiple blocks. ## Render a layout ### Configure layout The default, built-in page layout has only one zone. You can create other layouts in configuration, under the `ibexa_fieldtype_page.layouts` key. To create a new layout called "Right sidebar", use the following [configuration](https://doc.ibexa.co/en/saas/administration/configuration/configuration/#configuration-files): ```yaml ibexa_fieldtype_page: layouts: sidebar: identifier: sidebar name: Right sidebar description: Main section with sidebar on the right thumbnail: /assets/images/layouts/sidebar.png template: '@ibexadesign/layouts/sidebar.html.twig' zones: first: name: First zone second: name: Second zone ``` ### Add layout template A layout template renders all the zones of the layout. Each zone must have a `data-ibexa-zone-id` attribute with the number of the zone. The best way to display blocks in the zone is to iterate over a blocks array and render the blocks in a loop. Each block must have the `landing-page__block block_{{ block.type }}` classes and the `data-ibexa-block-id="{{ block.id }}` attribute. To render the "Right sidebar" layout, add the following template to `templates/themes/my_theme/layouts/sidebar.html.twig`: ```html+twig
    {% if zones[0].blocks %} {% for block in zones[0].blocks %}
    {{ render_esi(controller('Ibexa\\Bundle\\FieldTypePage\\Controller\\BlockController::renderAction', { 'contentId': contentInfo.id, 'blockId': block.id, 'versionNo': versionInfo.versionNo, 'languageCode': field.languageCode })) }}
    {% endfor %} {% endif %}
    {% if zones[1].blocks %} {% for block in zones[1].blocks %}
    {{ render_esi(controller('Ibexa\\Bundle\\FieldTypePage\\Controller\\BlockController::renderAction', { 'contentId': contentInfo.id, 'blockId': block.id, 'versionNo': versionInfo.versionNo, 'languageCode': field.languageCode })) }}
    {% endfor %} {% endif %}
    ``` ## Render a block Every built-in page block has a default template, [which you can override](#override-default-block-templates). Every page block can also have multiple other templates. The editor chooses a template when creating a block in the Page Builder. > **Caution: Clear the persistence cache** > > Persistence cache must be cleared after any modifications have been made to the block config in Page Builder, such as adding, removing or altering the page blocks, block attributes, validators or views configuration. > > To clear the persistence cache, run `php bin/console cache:pool:clear ` command. The default cache pool is named `cache.tagaware.filesystem`. The default cache pool when running Redis or Valkey is named `cache.redis`. If you have customized the [persistence cache configuration](https://doc.ibexa.co/en/saas/infrastructure_and_maintenance/cache/persistence_cache/#what-is-cached), the name of your cache pool might be different. > > In prod mode, you also need to clear the symfony cache by running `./bin/console c:c`. In dev mode, the Symfony cache is rebuilt automatically. ### Block configuration You can add new block templates by using configuration, for example, for the Content List block: ```yaml ibexa_fieldtype_page: blocks: contentlist: views: custom: template: '@ibexadesign/blocks/contentlist.html.twig' name: Custom content list ``` > **Tip: Tip** > > Use the same configuration to provide a template for [custom blocks](https://doc.ibexa.co/en/saas/content_management/pages/create_custom_page_block/index.md) you create. ### Block template Create the block template file in the provided path, for example, `templates/themes/my_theme/blocks/contentlist.html.twig`: ```html+twig

    {{ parentName }}

    {% if contentArray|length > 0 %}
    {% for content in contentArray %} {% endfor %}
    {% endif %}
    ``` ### Override default block templates To override the default block template, create a new template. Place it in a path that mirrors the original default template from the bundle folder. For example: `templates/bundles/IbexaFieldTypePageBundle/blocks/contentlist.html.twig`. > **Tip: Tip** > > To use a different file structure when overriding default templates, add an import statement to the template. > > For example, in `templates/bundles/IbexaFieldTypePageBundle/blocks/contentlist.html.twig`: > > ```html+twig > {% import 'templates/blocks/contentlist/new_default.html.twig'} > ``` > > Then, place the actual template in the imported file `templates/blocks/contentlist/new_default.html.twig`. # Render content in PHP > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). Render content item from PHP and get the resulting HTML as a string. While in PHP, you may need to render the view of a content item (for example, for further treatment like PDF conversion, or because you're not in an HTML context). > **Caution: Caution** > > Avoid using PHP rendering in a controller as much as possible. You can access a view directly through the route `/view/content/{contentId}/{viewType}[/{location}]`. For example, on a fresh installation, you can access `/view/content/52/line` which returns a small piece of HTML with a link to the content that could be used in Ajax. If you need a controller to have additional information available in the template or to customize the `Response` object, define the controller in a [view configuration](https://doc.ibexa.co/en/saas/templating/templates/template_configuration/index.md) as shown in [Controllers](https://doc.ibexa.co/en/saas/templating/queries_and_controllers/controllers/index.md), enhance the View object and return it. The following example is a command outputting the render of a content for a view type in the terminal. It works only if the view doesn't refer to the HTTP request. It's compatible with the default installation views such as `line` or `embed`. To go further with this example, you could add some dedicated views not outputting HTML but, for example, plain text, [Symfony command styled text](https://symfony.com/doc/7.4/console/style.html#output-coloring) or Markdown. It doesn't work with a `full` view when the [page layout](https://doc.ibexa.co/en/saas/templating/templates/template_configuration/#page-layout) uses `app.request`, such as the out-of-the-box template. Create the command in `src/Command/ViewCommand.php`: ```php addOption('content-id', 'c', InputOption::VALUE_OPTIONAL, 'Content ID') ->addOption('location-id', 'l', InputOption::VALUE_OPTIONAL, 'Location ID') ->addOption('view-type', 't', InputOption::VALUE_OPTIONAL, 'View Type', 'line'); } protected function execute(InputInterface $input, OutputInterface $output): int { $contentId = $input->getOption('content-id'); $locationId = $input->getOption('location-id'); if (empty($contentId) && empty($locationId)) { throw new \InvalidArgumentException('No Content ID nor Location ID given'); } $viewParameters = [ 'viewType' => $input->getOption('view-type'), '_controller' => 'ibexa_content::viewAction', ]; if (!empty($locationId)) { $viewParameters['locationId'] = $locationId; } if (!empty($contentId)) { $viewParameters['contentId'] = $contentId; } // build view $contentView = $this->contentViewBuilder->buildView($viewParameters); // render view $renderedView = $this->templateRenderer->render($contentView); $output->writeln($renderedView); return 0; } } ``` > **Caution: Caution** > > As `Ibexa\Core\MVC\Symfony\View\Builder\ContentViewBuilder` and `Ibexa\Core\MVC\Symfony\View\Renderer\TemplateRenderer` aren't part of the public PHP API's `Ibexa\Contracts` namespace, they might change without notice. Use the command with some views: ```bash php bin/console app:view --content-id=52 php bin/console app:view --location-id=2 --view-type=embed ``` # Templates > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). Cohesivo uses the Twig template engine to customize the rendering of content in the site. You can customize the layout and look of your website with templates. Templates use the Twig template engine. > **Tip: Tip** > > Learn more about Twig templates from [Twig documentation](https://twig.symfony.com/doc/3.x/templates.html). ## Connecting templates Templates can inherit from other templates. Use this, for example, to inherit a general page layout including a [navigation menu](https://doc.ibexa.co/en/saas/templating/layout/add_navigation_menu/index.md) in article templates. To inherit from other templates, a template must extend the parent templates by using the [`extends()`](https://twig.symfony.com/doc/3.x/tags/extends.html) Twig function. To extend a parent template, the child template must contain Twig blocks. These blocks are inserted in the parent template in relevant places. For example, to extend the [general layout of the page](https://doc.ibexa.co/en/saas/templating/templates/template_configuration/#view-rules-and-matching), which includes, for example, header, footer, or navigation, in the child template place the content in a `content` block: ```html+twig {% extends '@ibexadesign/pagelayout.html.twig' %} {% block content %} {% endblock %} ``` The parent template (in this case, `pagelayout.html.twig`) must leave a place for this block: ```html+twig {% block content %} {% endblock %} ``` ## Template variables In templates, you can use variables related to the current content item, and general variables related to the current view and general application settings. > **Tip: Tip** > > For development purposes, you can list all available variables, or a single variable, and their values, by using the `dump()` Twig function: > > ```html+twig > {{ dump() }} > {{ dump(content) }} > ``` Main variables include: | Variable | Description | | ----------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- | | `content` | Content item, containing all Fields and version information (VersionInfo). | | `location` | Location object. Contains meta information on the Content (ContentInfo). | | `ibexa.siteaccess` | Current [SiteAccess](https://doc.ibexa.co/en/saas/multisite/multisite/index.md). | | `ibexa.rootLocation` | Root Location object. | | `ibexa.requestedUriString` | Requested URI string. | | `ibexa.systemUriString` | System URI string. System URI is the URI for internal content controller. If the current route isn't a URL alias, then the current PathInfo is returned. | | `ibexa.viewParameters` | View parameters as a hash. | | `ibexa.viewParametersString` | View parameters as a string. | | `ibexa.translationSiteAccess` | Translation SiteAccess for a given language (null if the SiteAccess cannot be found). | | `ibexa.availableLanguages` | List of available languages. | | `ibexa.configResolver` | [Config resolver](https://doc.ibexa.co/en/saas/administration/configuration/dynamic_configuration/#configresolver). | ### Custom template variables You can create custom Twig variables for use in templates. Set the variables per SiteAccess or SiteAccess group ([scope](https://doc.ibexa.co/en/saas/multisite/multisite_configuration/#scope)), or per content view. To configure a custom template variable per scope, use the `twig_variables` [configuration key](https://doc.ibexa.co/en/saas/administration/configuration/configuration/#configuration-files): ```yaml ibexa: system: site_group: twig_variables: custom_variable: 'variable_value' ``` You can access this variable directly in all templates in that scope: ```html+twig {{ custom_variable }} ``` Variables set for a specific content view (under `params`) are only available when this view is matched: ```yaml line: article: template: '@ibexadesign/line/article.html.twig' match: Identifier\ContentType: [article] params: custom_variable_per_view: 'variable_value' ``` Custom variables can be nested: ```yaml twig_variables: custom_variable: nested_variable: 'variable_value' ``` ```html+twig {{ custom_variable.nested_variable }} ``` You can use [Symfony Expression language](https://symfony.com/doc/7.4/expression_language.html) to access other values, for example: ```yaml params: custom_variable: "@=content.contentType.identifier" ``` > **Note: Note** > > A custom variable can overwrite an existing variable, so it's good practice to avoid existing variable names such as `content` or `location`. # Template configuration > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). Template configuration defines which templates are used for which content and in what cases. You configure how templates are used under the `ibexa.system..content_view` [configuration key](https://doc.ibexa.co/en/saas/administration/configuration/configuration/#configuration-files). The following example configuration defines template usage for several cases: ```yaml ibexa: system: site_group: page_layout: '@ibexadesign/pagelayout.html.twig' content_view: full: article: template: '@ibexadesign/full/article.html.twig' match: Identifier\ContentType: article blog_post: template: '@ibexadesign/full/blog_post.html.twig' controller: App\Controller\BlogController::showBlogPostAction match: Identifier\ContentType: [blog_post] terms: template: '@ibexadesign/full/terms_and_conditions.html.twig' match: Id\Content: 144 line: article: template: '@ibexadesign/line/article.html.twig' match: Identifier\ContentType: [article] ``` ## Scope The content view configuration must be placed under `ibexa.system.`. Scope defines the [SiteAccesses](https://doc.ibexa.co/en/saas/multisite/multisite/index.md) for which the configuration is valid. It may be a SiteAccess, a SiteAccess group, or one of the [generic configuration scopes](https://doc.ibexa.co/en/saas/multisite/multisite_configuration/#scope). ## Page layout `page_layout` defines the general layout of the whole site. Other templates can [extend the page layout](#page-layout). ```yaml page_layout: '@ibexadesign/pagelayout.html.twig' ``` ## View types The `ibexa.system..content_view` [configuration key](https://doc.ibexa.co/en/saas/administration/configuration/configuration/#configuration-files) defines rules for rendering content. Rules are grouped per *view type*. ```yaml ibexa: system: site_group: page_layout: '@ibexadesign/pagelayout.html.twig' content_view: full: ``` The default, built-in views are: - `full` - used when the content item is displayed by itself, as a full page - `line` - used when content is displayed as an item in a list, for example a list of the contents of a folder - `text_linked` - used for a text section which is a link - `embed` - used when one content item is embedded in another, as a block - `embed-inline` - used when a content item is embedded inline in another - `asset_image` - used when an image asset is embedded in another content item The built-in views have built-in default templates. You can define any other custom views. For each custom view, you must define a custom template. > **Tip: Direct path to previewing view types** > > You can preview content in a specific view type by using a direct path to the built-in view controller: > > `/view/content///true/` > > For example: > > `/view/content/55/embed/true/57` ## View rules and matching Each rule must have a name unique per view type. For each rule you must define the matching conditions. The `match` key can contain one or more [view matchers](https://doc.ibexa.co/en/saas/templating/templates/view_matcher_reference/index.md), including [custom ones](https://doc.ibexa.co/en/saas/templating/templates/create_custom_view_matcher/index.md). ```yaml blog_post: template: '@ibexadesign/full/blog_post.html.twig' controller: App\Controller\BlogController::showBlogPostAction match: Identifier\ContentType: [blog_post] ``` `template` indicates which template to use. `controller` indicates which [controller](https://doc.ibexa.co/en/saas/templating/queries_and_controllers/controllers/index.md) and which method to use when rendering the content. You can use it together with the `template` key, or without it. `params` can provide additional parameters to the content view. Use them, for example, with [Query types](https://doc.ibexa.co/en/saas/templating/queries_and_controllers/content_queries/#query-types) or to provide [custom Twig variables](https://doc.ibexa.co/en/saas/templating/templates/templates/#custom-template-variables) to the template. ### Combining matchers When you use more than one matcher in one rule, both conditions must match for the rule to apply. ```yaml match: Identifier\ContentType: [article, blog_post] Identifier\Section: news ``` In the example above, content which is either an article or a blog post is matched, but it must be in the "News" Section. ### Matching every content item When you use no matcher in a rule, this rule always match. Several values are available to declare no matcher: ```yaml match: ~ match: true match: [] ``` Such rules can be found in the [default template configuration](https://github.com/ibexa/core/blob/4.5/src/bundle/Core/Resources/config/default_settings.yml#L47). > **Tip: Tip** > > For example, you can ensure that any content item lacking a dedicated template isn't displayed in `full` view but is instead sent to a custom controller. > > ```yaml > site_group: > content_view: > full: > # Rules for content types and specific content items meant to be displayed in full view: > # … > # Rule for other content items not meant to be displayed in full view: > no_full_view: > controller: App\Controller\ViewController::noFullViewAction > template: '@ibexadesign/full/no_full_view.html.twig' > match: ~ > ``` > > This custom controller can also set the response status code to 404 using the following code: `$view->setResponse((new Response())->setStatusCode(404));`, and fetch reverse relations to provide suggestions on the error page. # View matcher reference > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). View matchers are used in template configuration to decide when to use which template and controller. You can use the following matchers to [match content views](https://doc.ibexa.co/en/saas/templating/templates/template_configuration/#view-rules-and-matching): | Identifier | Matches | | -------------------------------------------------------------- | ------------------------------------------------------------------------------------ | | [`Id\Content`](#idcontent) | ID number of the content item. | | [`Id\ContentType`](#idcontenttype) | ID number of the content type that the content item belongs to. | | [`Identifier\ContentType`](#identifiercontenttype) | Identifier of the content type that the content item belongs to. | | [`Id\ContentTypeGroup`](#idcontenttypegroup) | ID number of the group containing the content type that the content item belongs to. | | [`Id\Location`](#idlocation) | ID number of a Location. | | [`Id\LocationRemote`](#idlocationremote) | Remote ID number of a Location. | | [`Id\ParentContentType`](#idparentcontenttype) | ID number of the parent content type. | | [`Identifier\ParentContentType`](#identifierparentcontenttype) | Identifier of the parent content type. | | [`Id\ParentLocation`](#idparentlocation) | ID number of the parent Location. | | [`Id\Remote`](#idremote) | Remote ID of a content item. | | [`Id\Section`](#idsection) | ID number of the Section that the content item belongs to. | | [`Identifier\Section`](#identifiersection) | Identifier of the Section that the content item belongs to. | | [`Depth`](#depth) | Depth of the Location. The depth of a top level Location is 1. | | [`UrlAlias`](#urlalias) | Virtual URL of the Location. | | [Product attribute value](#product-attribute-value) | Value of product attributes. | | [Product code](#product-code) | Product code. | | [Product type](#product-type) | Product type. | | [Product availability](#product-availability) | Product availability. | | [Product](#product) | Whether the object is a product. | | [Product catalog root](#product-catalog-root) | Whether the Location is the root of a product catalog. | | [Taxonomy entry ID](#taxonomy-entry-id) | ID of taxonomy entry. | | [Taxonomy entry identifier](#taxonomy-entry-identifier) | Identifier of taxonomy entry. | | [Taxonomy entry level](#taxonomy-entry-level) | Level of taxonomy entry. | | [Taxonomy type](#taxonomy-type) | Taxonomy type. | > **Tip: Tip** > > Each matcher has a scalar value or an array of scalar values. When an array is passed, it matches on one of its values. > > You can also create [custom view matchers](https://doc.ibexa.co/en/saas/templating/templates/create_custom_view_matcher/index.md). ## Id\\Content Matches the ID number of a content item. ```yaml match: Id\Content: 145 ``` ## Id\\ContentType Matches the ID number of a content type that the content item belongs to. ```yaml match: Id\ContentType: 2 ``` ## Identifier\\ContentType Matches the identifier of the content type that the content item belongs to. ```yaml match: Identifier\ContentType: [blog_post] ``` ## Id\\ContentTypeGroup Matches the ID number of the content type Group that the content item belongs to. ```yaml match: Id\ContentTypeGroup: 1 ``` ## Id\\Location Matches the ID number of a location. In the case of a content item, matched against the main location. ```yaml match: Id\Location: 144 ``` ## Id\\LocationRemote Matches the Remote ID number of a location. In the case of a content item, matched against the main location. ```yaml match: Id\LocationRemote: 5b1e33529082b68ad3a41b9089136a0a ``` ## Id\\ParentContentType Matches the ID number of the parent content type. In the case of a content item, matched against the main location. ```yaml match: Id\ParentContentType: 42 ``` ## Identifier\\ParentContentType Matches the identifier of the parent content type. In the case of a content item, matched against the main location. ```yaml match: Identifier\ParentContentType: blog ``` ## Id\\ParentLocation Matches the ID number of the parent location. In the case of a content item, matched against the main location. ```yaml match: Id\ParentLocation: 2 ``` ## Id\\Remote Matches the remote ID number of a content item. ```yaml match: Id\Remote: 145 ``` ## Id\\Section Matches the ID number of the section that the content item belongs to. ```yaml match: Id\Section: 1 ``` ## Identifier\\Section Matches the identifier of the section that the content item belongs to. ```yaml match: Identifier\Section: standard ``` ## Depth Matches the depth of the location. The depth of a top level location is 1. ```yaml match: Depth: 2 ``` ## UrlAlias Matches the virtual URL of the location. Matches when the URL alias of the location starts with the value passed. ```yaml match: UrlAlias: 'terms-and-conditions' ``` ## Product attribute value `Ibexa\Contracts\ProductCatalog\ViewMatcher\ProductBased\AttributeValue` matches the value of product attributes. ```yaml match: '@Ibexa\Contracts\ProductCatalog\ViewMatcher\ProductBased\AttributeValue': { width: 20, height: 10 } ``` ## Product code `Ibexa\Contracts\ProductCatalog\ViewMatcher\ProductBased\ProductCode` matches the product code. ```yaml match: '@Ibexa\Contracts\ProductCatalog\ViewMatcher\ProductBased\ProductCode': ['DRE1536SF'] ``` ## Product type `Ibexa\Contracts\ProductCatalog\ViewMatcher\ProductBased\ProductType` matches the product type. ```yaml match: '@Ibexa\Contracts\ProductCatalog\ViewMatcher\ProductBased\ProductType': ['dress'] ``` ## Product availability `Ibexa\Contracts\ProductCatalog\ViewMatcher\ProductBased\IsAvailable` matches the availability of a product. Refers to the existence of availability, not to whether the product is in stock. ```yaml match: '@Ibexa\Contracts\ProductCatalog\ViewMatcher\ProductBased\IsAvailable': true ``` ## Product `Ibexa\Contracts\ProductCatalog\ViewMatcher\ProductBased\IsProduct` matches when the object is a product. ```yaml match: '@Ibexa\Contracts\ProductCatalog\ViewMatcher\ProductBased\IsProduct': ~ ``` ## Product catalog root `Ibexa\Contracts\ProductCatalog\ViewMatcher\LocationBased\RootLocation` matches depending on whether the location is the root of a product catalog. ```yaml match: '@Ibexa\Contracts\ProductCatalog\ViewMatcher\LocationBased\RootLocation': true ``` ## Taxonomy entry ID `Ibexa\Taxonomy\View\Matcher\TaxonomyEntryBased\Id` matches based on an ID of the taxonomy entry. ```yaml match: '@Ibexa\Taxonomy\View\Matcher\TaxonomyEntryBased\Id': [1, 2, 3] ``` ## Taxonomy entry identifier `Ibexa\Taxonomy\View\Matcher\TaxonomyEntryBased\Identifier` matches based on an identifier of the taxonomy entry. ```yaml match: '@Ibexa\Taxonomy\View\Matcher\TaxonomyEntryBased\Identifier': ['spring', 'events', 'devices'] ``` ## Taxonomy entry level `Ibexa\Taxonomy\View\Matcher\TaxonomyEntryBased\Level` matches based on a level of the taxonomy entry. With this matcher, you can apply view rules based on a selection of taxonomy entry levels, by using the following logical operators: `<` , `>` , `<=`, `>=`, `=`. ```yaml match: '@@Ibexa\Taxonomy\View\Matcher\TaxonomyEntryBased\Level': '> 2' ``` ## Taxonomy type `Ibexa\Taxonomy\View\Matcher\TaxonomyEntryBased\Taxonomy` matches based on a type of taxonomy that the taxonomy entry belongs to. ```yaml match: '@Ibexa\Taxonomy\View\Matcher\TaxonomyEntryBased\Taxonomy': 'product_category' ``` # Create custom view matcher > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). You can create custom view matchers to configure template and controller usage for specific custom cases. In addition to the [built-in view matchers](https://doc.ibexa.co/en/saas/templating/templates/view_matcher_reference/index.md), you can also create custom matchers to use in [template configuration](https://doc.ibexa.co/en/saas/templating/templates/template_configuration/#view-rules-and-matching). To do it, create a matcher class that implements `Ibexa\Core\MVC\Symfony\Matcher\ContentBased\MatcherInterface`. ## Matcher class The matcher class must implement the following methods: - `matchLocation` - checks if a location object matches. - `matchContentInfo` - checks if a ContentInfo object matches. - `match` - checks if the View object matches. - `setMatchingConfig` - receives the matcher's config from the view rule. The following example shows how to implement an `Owner` matcher. This matcher identifies content items that have the provided owner or owners. ```php hasOwner($location->getContentInfo()); } /** * @throws \Ibexa\Contracts\Core\Repository\Exceptions\NotFoundException */ public function matchContentInfo(ContentInfo $contentInfo): bool { return $this->hasOwner($contentInfo); } /** * @throws \Ibexa\Contracts\Core\Repository\Exceptions\NotFoundException */ public function match(View $view): ?bool { if ($view instanceof LocationValueView) { return $this->matchLocation($view->getLocation()); } if ($view instanceof ContentValueView) { return $this->matchContentInfo($view->getContent()->contentInfo); } return false; } /** * @throws \Ibexa\Contracts\Core\Repository\Exceptions\NotFoundException */ private function hasOwner(ContentInfo $contentInfo): bool { $owner = $this->userService->loadUser($contentInfo->ownerId); return in_array($owner->login, $this->matchingUserLogins, true); } /** * @param array $matchingConfig */ public function setMatchingConfig($matchingConfig): void { if (!is_array($matchingConfig)) { throw new InvalidArgumentException('App\Owner view matcher configuration has to be an array'); } $this->matchingUserLogins = $matchingConfig; } } ``` The matcher checks whether the owner of the current content (by its ContentInfo or location) matches any of the values passed in configuration. ## Matcher service You configure your matcher as a service, tag it `ibexa.view.matcher`, and associate it with the identifier to use in view rules: ```yaml services: App\View\Matcher\Owner: autowire: true tags: - { name: ibexa.view.matcher, identifier: App\Owner } ``` ## View configuration To apply the matcher in view configuration, indicate the matcher by its identifier. The following configuration uses a special template to render articles owned by the users with provided logins: ```yaml ibexa_design_engine: design_list: my_design: [ my_theme ] ibexa: system: site_group: design: my_design content_view: full: editor_articles: template: '@ibexadesign/full/featured_article.html.twig' match: Identifier\ContentType: article App\Owner: [johndoe, janedoe] ``` > **Note: Note** > > If you use a matcher that is a service instead of a simple class, tag the service with `ibexa.view.matcher`. # Assets > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). Add assets (CSS, JS, image and other files) to your site and manage them using Webpack Encore. Assets enable you to add CSS, JS, image, or other files to your project, to style and customize its look and behavior. ## Asset files To add assets to the project, provide asset files (such as CSS or JS files) in the `assets` folder, for example under `assets/css` and `assets/js`. ## Configure assets All asset files must be added to `webpack.config.js` in the root folder, so that Webpack Encore can use them. To do it, use `Encore.addStyleEntry` for CSS files and `Encore.addEntry` for other files, such as JS: ```js Encore.addStyleEntry('style', [ path.resolve(__dirname, './assets/css/style.css'), ]); Encore.addEntry('script', [ path.resolve(__dirname, './assets/js/script.js'), ]); ``` ## Include assets in templates To include assets in your templates, add them to the template's `` tag, and provide the name of the asset entry you configured in `webpack.config.js`, for example: ```html+twig {{ encore_entry_link_tags('style') }} {{ encore_entry_script_tags('script') }} ``` > **Note: Note** > > After you add the asset files, clear the cache and run `yarn encore `. To include a single asset file in your template, for example an image, use the [`asset()`](https://symfony.com/doc/7.4/reference/twig_reference.html#asset) Twig function: ```html+twig ``` Place the image file in the `public/assets/images` folder. # Image variations > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). Configure image variations to scale, crop and otherwise modify rendered images. With image variations you can render different versions of one image by means of scaling, cropping and other filters. Built-in image variations include four versions that provide the image at a specific scale: `tiny`, `small`, `medium`, and `large`. You can also create custom image variations. See [Render images](https://doc.ibexa.co/en/saas/templating/embed_and_list_content/render_images/index.md) for an example of variation name usage as `alias` parameter when rendering an image field. ## Custom image variations Image variation configuration is [SiteAccess](https://doc.ibexa.co/en/saas/multisite/multisite/index.md)-aware. Place it under the `image_variations` [configuration key](https://doc.ibexa.co/en/saas/administration/configuration/configuration/#configuration-files) per [scope](https://doc.ibexa.co/en/saas/multisite/multisite_configuration/#scope): ```yaml ibexa: system: : image_variations: : reference: null filters: filter_name: - parameter1 - parameter2 ``` Variation name must be unique. It may contain characters, numbers, underscores (`_`) or hyphens (`-`), but no spaces. Each variation takes the following parameters: - `reference` - (optional) name of a reference variation to base the variation on. If set to `null` or `~`, the variation takes the original image for reference. - `filters` - array of variation filters and their parameters. - `post_processors` - used to reduce the final image size and to improve load performance of assets. ## Available variation filters In addition to [filters exposed by LiipImagineBundle](https://symfony.com/bundles/LiipImagineBundle/2.x/filters.html), the following ones are available: | Filter name | Parameters | Description | | ------------------------------ | ------------------------------------------ | -------------------------------------------------------------------------------------------------- | | `geometry/scaledownonly` | `[width, height]` | Scales image down to fit the provided width/height. Preserves aspect ratio. | | `geometry/scalewidthdownonly` | `[width]` | Scales image down to fit the provided width. Preserves aspect ratio. | | `geometry/scaleheightdownonly` | `[height]` | Scales image down to fit the provided height. Preserves aspect ratio. | | `geometry/scalewidth` | `[width]` | Scales image width, both up and down. Preserves aspect ratio. | | `geometry/scaleheight` | `[height]` | Scales image height, both up and down. Preserves aspect ratio. | | `geometry/scale` | `[width, height]` | Scales image size to the provided width and height, both up and down. Preserves aspect ratio. | | `geometry/scaleexact` | `[width, height]` | Scales image to exactly fit the provided width and height. Doesn't preserve aspect ratio. | | `geometry/scalepercent` | `[widthPercent, heightPercent]` | Scales width and height by the provided percent values. Doesn't preserve aspect ratio. | | `geometry/crop` | `[width, height, startX, startY]` | Crops the image. The result has the provided width/height, starting at the provided startX/startY | | `border` | `[thickBorderX, thickBorderY, color=#000]` | Adds a border around the image. Thickness is defined in px. Color is `#000` by default. | | `filter/noise` | `[radius=0]` | Smooths the contours of an image (`imagick`/`gmagick` only). `radius` is in px. | | `filter/swirl` | `[degrees=60]` | Swirls the pixels of the center of the image (`imagick`/`gmagick` only). `degrees` defaults to 60. | | `resize` | {size: `[width, height]`} | Resize filter (provided by LiipImagineBundle). | | `colorspace/gray` | N/A | Converts the image to grayscale. | > **Note: Note** > > After you change the image variation configuration, remove the existing variations with the `liip:imagine:cache:remove` command and provide the variation name: > > ```bash > php bin/console liip:imagine:cache:remove --filter=large > ``` > > Next, clear the cache. > > You can also remove all generated image variations: > > ```bash > php bin/console liip:imagine:cache:remove -v > ``` # Twig Components > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). Twig components allow you to inject any custom widgets into selected places of the user interface. Twig Components are widgets (for example, **My dashboard** blocks from Headless edition) and HTML code (for example, a tag for loading JS or CSS files) that you can inject into the existing templates to customize and extend the user interface. They are combined into groups that are rendered in designated templates. Twig Component groups are available for the [back office](https://doc.ibexa.co/en/saas/administration/back_office/back_office_elements/custom_components/index.md). To learn which groups are available in a given view, use the [integration Symfony Profiler](#symfony-profiler-integration). ## Create Twig Component You can create Twig Components in one of two ways: ### PHP code Create a class that implements the `\Ibexa\Contracts\TwigComponents\ComponentInterface` interface. Register it as a service by using the `AsTwigComponent` attribute or the `ibexa.twig.component` service tag: **PHP Attribute** ```php ` tag](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/script) | `script` | | [Stylesheet](https://github.com/ibexa/twig-components/blob/6.0/src/lib/Component/LinkComponent.php) | Renders a [`` tag](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/link) | `stylesheet` | | [Template](https://github.com/ibexa/twig-components/blob/6.0/src/lib/Component/TemplateComponent.php) | Renders a Twig template | `template` | For the menu component, the following properties are available: | parameter | type | required | description | | --------- | -------- | -------- | ------------------------------ | | name | string | yes | Menu name | | options | array | no | Options passed to menu builder | | path | string[] | no | Path to starting node | | template | string | no | Template used to render menu | | depth | int | no | Menu depth limit | The menu component, same as [back office menus](https://doc.ibexa.co/en/saas/administration/back_office/back_office_menus/back_office_menus/index.md), relies on the [KnpMenuBundle](https://symfony.com/bundles/KnpMenuBundle/current/index.html). For more information about the available properties, refer to the [official documentation of the bundle](https://symfony.com/bundles/KnpMenuBundle/current/index.html#create-your-first-menu). ## Example The following example shows how you can use each of the built-in components to customize the back office: ```yaml ibexa_twig_components: admin-ui-user-menu: custom-controller-component: type: controller arguments: controller: '\App\Controller\MyController::requestAction' parameters: parameter1: 'custom' parameter2: true custom-html-component: type: html priority: 0 arguments: content: 'Hello world!' duplicated_user_menu: type: menu arguments: name: ezplatform_admin_ui.menu.user template: '@ibexadesign/ui/menu/user.html.twig' depth: 1 admin-ui-script-head: custom-script-component: type: script arguments: src: 'https://doc.ibexa.co/en/latest/js/custom.js' crossorigin: anonymous defer: false async: true integrity: sha384-Ewi2bBDtPbbu4/+fs8sIbBJ3zVl0LDOSznfhFR/JBK+SzggdRdX8XQKauWmI9HH2 type: text/javascript admin-ui-stylesheet-head: custom-link-component: type: stylesheet arguments: href: 'https://fonts.googleapis.com/css?family=Roboto:300,300i,400,400i,700,700i%7CRoboto+Mono:400,400i,700,700i&display=fallback' rel: stylesheet crossorigin: anonymous integrity: sha384-LN/mLhO/GN6Ge8ZPvI7uRsZpiXmtSkep+aFlJcHa8by4TvA34o1am9sa88eUzKTD type: text/css admin-ui-global-search: custom-template-component: type: template priority: 50 arguments: template: '@ibexadesign/ui/component/user_thumbnail/user_thumbnail.html.twig' parameters: user_content: name: "Thumbnail" thumbnail: resource: https://placecats.com/100/100 ``` ## Render Twig Components Render both single Twig Components and whole groups using the dedicated Twig functions. You can modify the Component rendering process by: - listening to one of the [related events](https://doc.ibexa.co/en/saas/api/event_reference/twig_component_events/index.md) - decorating the `\Ibexa\Contracts\TwigComponents\Renderer\RendererInterface` service ## Symfony Profiler integration Use the built-in integration with [Symfony Profiler](https://symfony.com/doc/7.4/profiler.html) to see which Twig Components have been rendered in a given view. In the **Cohesivo** tab you can find: - the list of all rendered Twig Component groups by the given view, including empty groups - the list of rendered Twig Components with information about the group they belong to ![Symfony Profiler showing the list of rendered Twig Components in a back office view](https://doc.ibexa.co/en/saas/templating/img/twig_components_symfony_profiler.png "Symfony Profiler showing the list of rendered Twig Components in a back office view") # URLs and routes > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). Add links to content items or specific built-in and custom routes in your templates. To link to a [Location](https://doc.ibexa.co/en/saas/content_management/locations/index.md) or [Content item](https://doc.ibexa.co/en/saas/content_management/content_model/#content-items), use the `ibexa_path()` Twig function. You need to provide the function with a location, content, ContentInfo, or [RouteReference](#routereference) object: ```html+twig

    Location

    Content Info

    ``` Use `ibexa_url()` to get an absolute URL to a content item or location: ```html+twig

    Location

    ``` ## RouteReference You can use the `ibexa_route()` Twig function to create a RouteReference object based on the provided information. A RouteReference contains a route with its parameters and can be modified after it's created. Here, the route is based on the ID of the location. ```html+twig {% set routeReference = ibexa_route("ibexa.url.alias", { 'locationId': 2 }) %}

    Route

    ``` A route can also be based on the ID of the content item. The resulting link points to the content item's main location. ```html+twig {% set routeReference = ibexa_route("ibexa.url.alias", { 'contentId': 456 }) %}

    Route

    ``` For cross-SiteAccess links, you can pass the parameter `siteaccess` with a SiteAccess identifier. ```html+twig {% set routeReference = ibexa_route("ibexa.url.alias", { 'contentId': 456, 'siteaccess': 'shop' }) %}

    Route

    ``` With `ibexa_route()` you can modify the route contained in RouteReference after creation, for example, by providing additional parameters: ```html+twig {% set routeReference = ibexa_route("ibexa.url.alias", { 'locationId': 2 }) %} {% do routeReference.set("param", "param-value") %} ``` You can also use `ibexa_route()` to create links to predefined routes, such as the `ibexa.search` route that leads to a search form page: ```html+twig Search ``` ## File download links To provide a download link for a file, use `ibexa_route()` with the `ibexa_content_download` route: ```html+twig {% set download_route = ibexa_route('ibexa_content_download', { 'content': file, 'fieldIdentifier': 'file', }) %} Download ``` ## Route list The following built-in routes are available for the front of the website. > **Tip: Tip** > > To view all routes existing in the system, including internal and back office related ones, run: > > ```bash > php bin/console debug:router > ``` ### Registration | Route name | Path | Description | | -------------------------------------------------------------------------- | -------------------------------------------- | ----------------------------------------- | | `ibexa.user.user_register` | `/user/register` | User registration form | | `ibexa.user.register_confirmation` `ibexa.user.user_register_confirmation` | `/register-confirm` `/user/register-confirm` | Confirmation page after user registration | ### Login | Route name | Path | Description | | ---------- | --------- | ------------------------------------------------------------------------------------ | | `login` | `/login` | [Login form](https://doc.ibexa.co/en/saas/templating/layout/add_login_form/index.md) | | `logout` | `/logout` | Logging out the current user | ### Password | Route name | Path | Description | | -------------------------------------- | --------------------------------- | ----------------------------------------------------------------------------------------------------------------- | | `ibexa.user_profile.change_password` | `/user/change-password` | Form for password change | | `ibexa.user.forgot_password` | `/user/forgot-password` | [Form for password resetting](https://doc.ibexa.co/en/saas/templating/layout/add_forgot_password_option/index.md) | | `ibexa.user.forgot_password.migration` | `/user/forgot-password/migration` | Form for resetting password after expiration | | `ibexa.user.forgot_password.login` | `/user/forgot-password/login` | Form for resetting password based on login instead of email address | | `ibexa.user.reset_password` | `/user/reset-password/{hashKey}` | Form for resetting password based on a generated link | ### Content | Route name | Path | Description | | ------------------------------- | ------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `ibexa_content_download` | `/content/download/{contentId}/{fieldIdentifier}/{filename}` | Downloading a binary file | | `ibexa.content.create_no_draft` | `/content/create/nodraft/{contentTypeIdentifier}/{language}/{parentLocationId}` | [Creating a content item without using a draft](https://doc.ibexa.co/en/saas/content_management/user_generated_content/#creating-a-content-item-without-using-a-draft) | | `ibexa.content.draft.edit` | `/content/edit/draft/{contentId}/{versionNo}/{language}/{locationId}` | [Editing a content item](https://doc.ibexa.co/en/saas/content_management/user_generated_content/#editing-a-content-item) | | `ibexa.content.draft.create` | `/content/create/draft/{contentId}/{fromVersionNo}/{fromLanguage}` | [Creating a new draft](https://doc.ibexa.co/en/saas/content_management/user_generated_content/#creating-a-new-draft) | ### Search | Route name | Path | Description | | -------------- | --------- | ----------- | | `ibexa.search` | `/search` | Search form | # Design engine > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). Design engine allows you to use different SiteAccess-aware themes in your site. You can use multiple different designs (theme lists) in your installation. You can set up different designs per SiteAccess or SiteAccess group. Designs are configured under the `ibexa_design_engine.design_list` [configuration key](https://doc.ibexa.co/en/saas/administration/configuration/configuration/#configuration-files): ```yaml ibexa_design_engine: design_list: my_design: [theme2, theme1, theme0] another_design: [theme3, theme0] ``` To indicate when to use a design, configure it under `ibexa.system.`: ```yaml ibexa: system: : design: my_design ``` Each scope can use only one design. ## Design theme list A theme is a set of directories to look for templates in. At application level, theme's templates are placed in a directory under `templates/themes` which has the same name as the theme. For example, templates placed in `templates/themes/standard` directory are automatically added to the `standard` theme. > **Caution: Caution** > > After you create a new directory with a theme in `templates/themes`, you must clear the cache (`php bin/console cache:clear`), even if you work in the dev environment. The order of themes in a design is important. The design engine attempts to apply the first theme in configuration (for example, `theme2`). If it cannot find the required template or asset in this theme, it proceeds to the next theme in the list (for example, `theme1` then `theme0` and finally the default `standard`). The `@ibexadesign` keyword in template paths is the way to use this feature. When the design engine finds `@ibexadesign`, it loops over the theme list of the current design and checks whether the template for the given theme exists in its paths. For example, `@ibexadesign/pagelayout.html.twig` means that this template is searched at locations like `templates/themes/theme2/pagelayout.html.twig`, `templates/themes/theme1/pagelayout.html.twig`, `templates/themes/theme0/pagelayout.html.twig` and then `templates/themes/standard/pagelayout.html.twig`. You can use this behavior to override only some templates from the main theme of your website. Do this, for example, when you create a SiteAccess with a special design for a campaign. > **Tip: Tip** > > You can check the final design theme lists with the following command: > > ```bash > php bin/console debug:container --parameter=ibexa.design.list --format=json > ``` ## Additional configuration ### Additional theme paths You can add any Twig template directory to the theme configuration. You can use it if you want to define templates from third-party bundles as part of one of your themes. To do it, set the `ibexadesign.templates_theme_paths` parameter: ```yaml ibexa_design_engine: design_list: my_design: [my_theme] templates_theme_paths: my_theme: - '%kernel.project_dir%/vendor///Resources/views' ``` Theme directories that you define have priority over the ones defined in `templates_theme_paths`. This ensures that it's always possible to override a template at the application level. You can also add a global override directory, by listing paths without assigning them to a theme: ```yaml ibexa_design_engine: templates_override_paths: - '%kernel.project_dir%/src/' ``` > **Tip: Tip** > > You can check the final template directory list per theme with the following command: > > ```bash > php bin/console debug:container --parameter=ibexa.design.templates.path_map --format=json > ``` > > `_override` is a theme added at the beginning of the current design theme list at template path resolution time. ### Asset resolution In production environments, to improve performance, asset resolution is done at compilation time. In development environments, assets are resolved at runtime. You can change this behavior by setting `disable_assets_pre_resolution`: ```yaml ibexa_design_engine: disable_assets_pre_resolution: true ``` # Add new design > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). Add a new design for a special marketing campaign site. To create different designs for different version of the website, you configure different sites based on the [SiteAccess](https://doc.ibexa.co/en/saas/multisite/multisite/index.md) content. This example shows how to prepare a site for a "Summer Sale" marketing campaign and provide it with a distinct design. ## Configure a new SiteAccess First, in the SiteAccess configuration, under the `ibexa.siteaccess` [configuration key](https://doc.ibexa.co/en/saas/administration/configuration/configuration/#configuration-files), add the `campaign` SiteAccess: ```yaml ibexa: siteaccess: list: - import - site - admin - corporate - campaign groups: site_group: [import, site, campaign] storefront_group: [site] corporate_group: [corporate] default_siteaccess: site ``` Adding the `campaign` SiteAccess to [`site_group`](https://doc.ibexa.co/en/saas/multisite/multisite_configuration/#siteaccess-groups) enables you to add common configuration for both SiteAccesses at the same time. > **Tip: Tip** > > For details about configuring different site roots and matching SiteAccesses, see [Set up campaign SiteAccess](https://doc.ibexa.co/en/saas/multisite/set_up_campaign_siteaccess/index.md). ## Add themes Next, configure a new `summersale` design for this theme, also named `summersale`: ```yaml ibexa_design_engine: design_list: summersale: [summersale] ``` Notice that the `standard` theme is automatically added at the end of the `summersale` design's theme list. Ensure that the `campaign` site uses this design (while the default `site` uses the default `standard` design). ```yaml ibexa: system: campaign: languages: [eng-GB] design: summersale site: languages: [eng-GB] design: standard ``` ## Add templates Now, create templates for the two sites. Templates for the main site should be placed in `templates/themes/standard`, and templates for the campaign site in `templates/themes/summersale`. First, modify the built-in general [page layout](https://doc.ibexa.co/en/saas/templating/templates/template_configuration/#page-layout) `templates/themes/standard/pagelayout.html.twig` by including a header and a footer section: ```html+twig {% include '@ibexadesign/parts/header.html.twig' %} {% block content %} {% endblock %} {% include '@ibexadesign/parts/footer.html.twig' %} {% block javascripts %} ``` `@ibexadesign` in the template paths points to a template relevant for the current design. In case of `site`, the template used for the header is `templates/themes/standard/parts/header.html.twig`. Create both the header and the footer template, for example: ```html+twig ``` ```html+twig
    Copyright Acme SA
    ``` Now, create templates for content, for example for an article, that [extend the page layout](https://doc.ibexa.co/en/saas/templating/templates/templates/#connecting-templates): ```html+twig {% extends '@ibexadesign/pagelayout.html.twig' %} {% block content %} {% endblock %} ``` Configure the content view so that both sites, the main one and the campaign, use this template. To do it, use the `site_group` that both sites belong to: ```yaml ibexa: system: site_group: content_view: full: article: template: '@ibexadesign/full/article.html.twig' match: Identifier\ContentType: [ article ] ``` Now, create an Article content item and preview it on the front page. You should see the article with a header and footer that you defined for the main site. ## Override templates Now, you need to override the header of the site to fit the campaign. Create a separate `templates/themes/summersale/parts/header.html.twig` file with different content, for example: ```html+twig ``` Preview the Article through the `campaign` SiteAccess: `/campaign/`. You can see that the page uses the campaign header, while the rest of the layout, including the footer, is the same as in the main site. This is because you defined `standard` design as fallback for this SiteAccess: ```yaml ibexa_design_engine: design_list: summersale: [summersale] ``` In this case, if the design engine cannot find a template for the current design, it uses the template from the next configured design. In the case above, the engine doesn't find the footer template for the `campaign` SiteAccess, so it uses the one from `standard`. This way you don't need to provide all templates for a new design, but only those that you want to be different than the fallback one. # Content queries > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). Query content by using Query types and content query field. With content queries you can find and render specific content according to criteria that you define. You can use queries to list or embed content items, such as: - [children in a folder](https://doc.ibexa.co/en/saas/templating/embed_and_list_content/list_content/#list-children-with-query-type) - related articles - [most recent blog posts](https://doc.ibexa.co/en/saas/templating/queries_and_controllers/create_custom_query_type/index.md) - recommended products Content queries use the built-in Query controller which simplifies querying. For more complex cases, you can build custom [controllers](https://doc.ibexa.co/en/saas/templating/queries_and_controllers/controllers/index.md). ## Query types The Query controller offers a set of [built-in Query types](https://doc.ibexa.co/en/saas/templating/queries_and_controllers/built-in_query_types/index.md). You can use them in the content view configuration, or in the [content query field](#content-query-field). You can also write [custom Query types](https://doc.ibexa.co/en/saas/templating/queries_and_controllers/create_custom_query_type/index.md) for the cases that aren't covered by the built-in ones. ### Query type configuration To use a Query type, select the Query controller (`ibexa_query`) in the [content view configuration](https://doc.ibexa.co/en/saas/templating/templates/template_configuration/index.md) and select the Query type under `params.query.query_type`: ```yaml folder: controller: ibexa_query::contentQueryAction template: '@ibexadesign/full/folder.html.twig' params: query: query_type: 'Children' parameters: content: '@=content' assign_results_to: items match: Identifier\ContentType: folder ``` Use one of the following Query controller methods: - `locationQueryAction` runs a location Search - `contentQueryAction` runs a content Search - `contentInfoQueryAction` runs a ContentInfo search - `pagingQueryAction` returns a `PagerFanta` object and can be used to quickly [paginate query results](#pagination) See the [Search](https://doc.ibexa.co/en/saas/search/search/index.md) documentation page for more details about different types of search. All Query types take the following parameters: - `query_type` is the name of the Query type to use. - `parameters` can include: - arbitrary values - expressions based on the `content`, `location` and `view` variables. For example, `@=location.id` is evaluated to the current Location's ID. - `assign_results_to` declares the Twig variable that contains the search results. > **Tip: Tip** > > Search results are a `SearchResult` object, which contains `SearchHit` objects. To get the content or Locations that are in search results, you access the `valueObject` of the `SearchHit`. ### Pagination To paginate the results of a query, use the `pagingQueryAction` of the Query controller and assign a limit per page in `params.query.limit`: ```yaml content_view: full: folder: controller: ibexa_query::pagingQueryAction template: '@ibexadesign/full/folder.html.twig' params: query: query_type: 'Children' parameters: content: '@=content' assign_results_to: items limit: 3 match: Identifier\ContentType: folder ``` Use the [`pagerfanta`](https://www.babdev.com/open-source/packages/pagerfanta/docs/3.x/intro) function to render pagination controls: ```html+twig {% for item in items %} {{ ibexa_render(item.valueObject) }} {% endfor %} {{ pagerfanta(items, 'twitter_bootstrap5', { 'routeName': 'ibexa.url.alias', 'routeParams': {'location': location } }) }} ``` ## Content query field The [Content query field](https://doc.ibexa.co/en/saas/content_management/field_types/field_type_reference/contentqueryfield/index.md) is a field that defines a query. The results of the query are available in the field value. ![Content query field definition](https://doc.ibexa.co/en/saas/templating/img/content_query_field_definition.png) ### Query type When adding the field to a content type definition, select the Query type in the **Query type** dropdown. All Query types in the application are available, both [built-in](https://doc.ibexa.co/en/saas/templating/queries_and_controllers/built-in_query_types/index.md) and [custom ones](https://doc.ibexa.co/en/saas/templating/queries_and_controllers/create_custom_query_type/index.md). ### Returned types Select the content type of items you want to return in the **Returned type** dropdown. To take it into account, your Query type must filter on the content type. Provide the selected content type through the `returnedType` variable: ```yaml contentType: '@=returnedType' ``` ### Pagination Select **Enable pagination** and set the number of items per page to paginate the results. You can override the pagination settings from field definition by setting the `enablePagination`, `disablePagination` or `itemsPerPage` parameters when rendering the content query field: ```html+twig {{ ibexa_render_field(content, 'query', { location: location|default(null), 'parameters': { 'enablePagination': true, 'itemsPerPage': 8 } }) }} ``` You can also define an offset for the results. Provide the offset in the Query type, or in parameters: ```yaml offset: 3 ``` If pagination is disabled and an offset value is defined, the query's offset is added to the offset calculated for a page. For example, with `offset` 5 and `itemsPerPage` 10, the first page starts with 5, the second page starts with 15, and so on. Without offset defined, pagination defines the starting number for each page. For example, with `itemsPerPage` 10, first page starts with 0, second page starts with 10, and so on. ### Parameters The following variables are available in parameter expressions: - `returnedType` - the identifier of the content type selected in the **Returned type** dropdown - `content` - the current content item - `location` - the current Location of the content item - `mainLocation` - the main Location of the content item - `contentInfo` - the current content item's ContentInfo ### Content view configuration To render a content query field, in the content view configuration, use the `content_query_field` view type: ```yaml content_view: content_query_field: blog: template: '@ibexadesign/content_query/blog_posts.html.twig' match: Identifier\ContentType: blog '@Ibexa\FieldTypeQuery\ContentView\FieldDefinitionIdentifierMatcher': query ``` The identifier of the content query field must be matched by using the `'@Ibexa\FieldTypeQuery\ContentView\FieldDefinitionIdentifierMatcher'` matcher. Query results are provided to the template in the `items` variable. See [List content](https://doc.ibexa.co/en/saas/templating/embed_and_list_content/list_content/#list-children-in-content-query-field) for an example of using the content query field. # Built-in Query types > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). Use built-in Query types to quickly query content items in templates. ## General Query type parameters All built-in Query types take the following optional parameters: - `limit` - maximum number of results to return - `offset` - offset for search hits, used for paginating the results - `sort` - [sort order](#sort-order) - `filter` - additional query filters: - `content_type` - return only results of given content types - `visible_only` - return only visible results (default `true`) - `siteaccess_aware` - return only results limited to the current SiteAccess root (default `true`) For example: ```yaml params: query: query_type: 'Children' parameters: content: '@=content' filter: content_type: ['blog_post'] visible_only: false limit: 5 offset: 2 sort: 'content_name asc, date_published desc' assign_results_to: items ``` ### Sort order To provide a sort order to the `sort` parameter, use names of the Sort Clauses. To find them, refer to [Sort Clause](https://doc.ibexa.co/en/saas/search/sort_clause_reference/sort_clause_reference/index.md) and the [relevant Sort Clause class](https://github.com/ibexa/core/blob/6.0/src/bundle/Core/Resources/config/sort_spec.yml#L29) ## Children The `Children` Query type retrieves children of the given location. It takes `location` or `content` as parameters. ```yaml params: query: query_type: 'Children' parameters: content: '@=content' assign_results_to: items ``` > **Tip: Tip** > > For an example of using the `Children` Query type, see [List content](https://doc.ibexa.co/en/saas/templating/embed_and_list_content/list_content/#list-children-with-query-type). ## Siblings The `Siblings` Query type retrieves locations that have the same parent as the provided content item or location. It takes `location` or `content` as parameters. ```yaml params: query: query_type: 'Siblings' parameters: content: '@=content' assign_results_to: items ``` > **Tip: Tip** > > For an example of using the `Siblings` Query type, see [Embed related content](https://doc.ibexa.co/en/saas/templating/embed_and_list_content/embed_content/#embed-siblings-with-query-type). ## Ancestors The `Ancestors` Query type retrieves all ancestors (direct parents and their parents) of the provided location. It takes `location` or `content` as parameters. ```yaml params: query: query_type: 'Ancestors' parameters: content: '@=content' assign_results_to: items ``` ## RelatedToContent The `RelatedToContent` Query type retrieves content that is a reverse relation to the provided content item. > **Tip: Tip** > > Reverse relations mean that the Query type shows content items that are *related to* the provided content item. For example, if a blog post contains a link to an article, you can use a `RelatedToContent` query to find the blog post from the article. To find all relations of a content item (in this example, all content that the blog post is related to), refer to [Embed content](https://doc.ibexa.co/en/saas/templating/embed_and_list_content/embed_content/#embed-relations-with-a-custom-controller). It takes `content` or `field` as required parameters. `field` indicates the Relation or RelationList field that contains the relations. ```yaml params: query: query_type: 'RelatedToContent' parameters: content: '@=content' field: 'relations' assign_results_to: items ``` ## GeoLocation The `GeoLocation` Query type retrieves content by distance of the location provided in a MapLocation field. It takes the following parameters: - `field` - MapLocation field identifier - `distance` - distance to check for - `latitude` and `longitude` - coordinates of the location to check distance to - (optional) `operator` - operator to check value against, by default `<=` ```yaml params: query: query_type: 'GeoLocation' parameters: field: 'location' distance: 200 latitude: '@=content.getFieldValue("location").latitude' longitude: '@=content.getFieldValue("location").longitude' operator: '<' assign_results_to: items ``` ## Catalog The `Catalog` Query type retrieves products belonging to a [catalog](https://doc.ibexa.co/en/saas/product_catalog/catalogs/index.md). It takes the following parameters: - `identifier` - identifier of the catalog ```yaml params: query: query_type: 'Catalog' parameters: identifier: 'promo' assign_results_to: products ``` # Create a custom Query type > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). Create a Query type to search for content according to your custom needs. If you need to perform a more complex query than the [built-in Query types](https://doc.ibexa.co/en/saas/templating/queries_and_controllers/built-in_query_types/index.md) allow, you can create a custom Query type. The following example shows how to create a custom Query type that renders the latest content items of selected Types. First, add the following `LatestContentQueryType.php` file to `src/QueryType`: ```php new Query\Criterion\LogicalAnd($criteria), 'sortClauses' => [ new Query\SortClause\DatePublished(Query::SORT_DESC), ], 'limit' => $parameters['limit'] ?? 10, ]); } public function getSupportedParameters() { return ['contentType', 'limit']; } } ``` > **Tip: Tip** > > When the custom Query type is in the `App` namespace, like in the example above, it's registered automatically as a service. Otherwise, register it with the `ibexa.query_type` service tag. The name defined in `getName()` is the one you use to identify the Query type in content view configuration. ```php public static function getName() { return 'LatestContent'; } ``` > **Caution: Caution** > > Query type name must be unique. The `getQuery()` method constructs the query based on Search Criteria and Sort Clauses. For more information, see [Content search](https://doc.ibexa.co/en/saas/search/search_api/index.md) and [Search reference](https://doc.ibexa.co/en/saas/search/criteria_reference/search_criteria_reference/index.md). The `getSupportedParameters()` method provides the parameters you can set in content view configuration. ```php public function getSupportedParameters() { return ['contentType', 'limit']; } ``` > **Note: Note** > > To have more control over the details of parameters, use the [Options resolver-based Query type](#options-resolver-based-query-type). Then, in the content view configuration, indicate that the content view should use the custom Query type: ```text content_view: full: latest: controller: ibexa_query::locationQueryAction template: '@ibexadesign/full/latest.html.twig' match: Identifier\ContentType: "latest" params: query: query_type: LatestContent parameters: contentType: [article, blog_post] assign_results_to: latest ``` ## Options resolver-based Query type Additionally, your custom Query type can extend the `OptionsResolverBasedQueryType` abstract class. This gives you more flexibility when defining parameters. In the `configureOptions()` method you can define the allowed parameters, their types and default values. ```php new Query\Criterion\LogicalAnd($criteria), 'sortClauses' => [ new Query\SortClause\DatePublished(Query::SORT_DESC), ], 'limit' => $parameters['limit'] ?? 10, ]); } protected function configureOptions(OptionsResolver $resolver): void { $resolver->setDefined(['contentType', 'limit']); $resolver->setAllowedTypes('contentType', 'array'); $resolver->setAllowedTypes('limit', 'int'); $resolver->setDefault('limit', 10); } } ``` > **Note: Note** > > In contrast with the previous example, a Query type that extends `OptionsResolverBasedQueryType` must implement the `doGetQuery()` method instead of `getQuery()`. # Controllers > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). Use controllers to customize rendering and querying content in your site. By configuring a controller you can modify and enhance the way in which the built-in content view controller renders content. You indicate which controller to use in the [content view configuration](https://doc.ibexa.co/en/saas/templating/templates/template_configuration/index.md), under the `controller` [configuration key](https://doc.ibexa.co/en/saas/administration/configuration/configuration/#configuration-files): ```yaml article: controller: App\Controller\RelationController::showContentAction template: '@ibexadesign/full/article.html.twig' match: Identifier\ContentType: article ``` ```php **Tip: Permissions for custom controllers** > > See [permission documentation](https://doc.ibexa.co/en/saas/permissions/permission_overview/#permissions-for-custom-controllers) for information about access control for custom controllers. # List content > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). Create and render a list of content items, for example, content in a folder or blog posts in a blog. To render a list of content items, for example, content in a folder, or blog posts in a blog, you can use one of two methods: - use a [Query type](#list-children-with-query-type) - create a content type with a [Content Query Field](#list-children-in-content-query-field) ## List children with Query type The following example shows how to render the children of a Folder. First, in the [content view configuration](https://doc.ibexa.co/en/saas/templating/templates/template_configuration/index.md), add the following view for the Folder content type: ```yaml content_view: full: folder: controller: ibexa_query::contentQueryAction template: '@ibexadesign/full/folder.html.twig' params: query: query_type: 'Children' parameters: content: '@=content' assign_results_to: items limit: 3 match: Identifier\ContentType: folder ``` `controller` defines which controller is used to render the view. In this example, it's the default [Query controller](https://doc.ibexa.co/en/saas/templating/queries_and_controllers/content_queries/index.md). ```yaml controller: ibexa_query::contentQueryAction ``` `params` define that you want to render the content by using the [`Children` Query type](https://doc.ibexa.co/en/saas/templating/queries_and_controllers/built-in_query_types/#children). This Query type automatically finds the children of the current content item. The results of the query are placed in the `items` variable, which you can use in templates. Then, place the following template in `templates/themes//full/folder.html.twig`: ```html+twig {% for item in items.searchHits %} {{ ibexa_render(item.valueObject, {'viewType': 'line'}) }} {% endfor %} ``` This template uses the `ibexa_render()` Twig function to render every child of the folder with the default template for the `line` view. ## List children in Content query Field A [Content query Field](https://doc.ibexa.co/en/saas/content_management/field_types/field_type_reference/contentqueryfield/index.md) is a field that defines a query. The following example shows how to use a Content query field to render a Blog with its Blog Post children. First, create a Blog content type that contains a Content query field with the identifier `query`. In the Field definition, select "Children" as the Query type. Provide the `content` parameter that the Query type requires: ```yaml content: '@=content' ``` You can paginate the query results by checking the **Enable pagination** box and selecting a limit of results per page. Select the content type you want to render (in this case, Blog Post) as **Returned type**. Then, in the content view configuration, add the configuration under `content_query_field`: ```yaml content_view: content_query_field: blog: template: '@ibexadesign/content_query/blog_posts.html.twig' match: Identifier\ContentType: blog '@Ibexa\FieldTypeQuery\ContentView\FieldDefinitionIdentifierMatcher': query ``` The `match` configuration matches both the content type and the identifier of the Content query field. Finally, in the template `templates/themes//content_query/blog_posts.html.twig`, render all results of the query: ```html+twig {% for item in items.searchHits %} {{ ibexa_render(item.valueObject, {'viewType': 'line'}) }} {% endfor %} ``` # Embed related content > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). Embed a content item in another using query types or controllers. To embed content in another content item, you query for it in the repository. There are two ways to query for a content item: - by using a [Query type](#embed-siblings-with-query-type) - by writing a [custom controller](#embed-relations-with-a-custom-controller) ## Embed siblings with Query type To render the Siblings of a content item (other content under the same parent Location), use the [Siblings Query type](https://doc.ibexa.co/en/saas/templating/queries_and_controllers/built-in_query_types/#siblings). To do it, use the built-in `ibexa_query` controller's `contentQueryAction`: ```yaml content_view: full: blog_post: controller: ibexa_query::contentQueryAction template: '@ibexadesign/full/blog_post.html.twig' params: query: query_type: 'Siblings' parameters: content: '@=content' limit: 3 sort: 'date_published desc' assign_results_to: items match: Identifier\ContentType: blog_post ``` The results of the Siblings query are placed in the `items` variable, which you can use in the template: ```html+twig {{ ibexa_content_name(content) }} {% for item in items.searchHits %} {{ ibexa_render(item.valueObject, {'viewType': 'line'}) }} {% endfor %} ``` ## Embed Relations with a custom controller You can use a custom controller for any situation where Query types aren't sufficient. ```yaml article: controller: App\Controller\RelationController::showContentAction template: '@ibexadesign/full/article.html.twig' params: accepted_content_types: [ 'article', 'test_target', 'test_source' ] match: Identifier\ContentType: article ``` This configuration points to a custom `RelationController` that should render all Articles with the `showContentAction()` method. ```php getParameter('accepted_content_types'); $location = $this->locationService->loadLocation($locationId); $contentInfo = $location->getContentInfo(); $versionInfo = $this->contentService->loadVersionInfo($contentInfo); $relationListIterator = new BatchIterator( new RelationListIteratorAdapter( $this->contentService, $versionInfo ) ); $items = []; foreach ($relationListIterator as $relationListItem) { if ($relationListItem->hasRelation() && in_array($relationListItem->getRelation()->getDestinationContentInfo()->getContentType()->identifier, $acceptedContentTypes)) { $items[] = $this->contentService->loadContentByContentInfo($relationListItem->getRelation()->getDestinationContentInfo()); } } $view->addParameters([ 'items' => $items, ]); return $view; } } ``` This controller uses the Public PHP API to get [the relations of a content item](https://doc.ibexa.co/en/saas/content_management/content_api/browsing_content/#relations) (lines 26-31). The controller takes the custom parameter called `accepted_content_types` (line 21), which is an array of content type identifiers that are rendered. This way you can control which content types you want to show or exclude. Finally, the controller returns the view with the results that were provided in the `items` parameter. You can use this parameter as a variable in the template: ```html+twig {% block content %}

    {{ ibexa_content_name(content) }}

      {% for item in items %} {{ ibexa_render(item, {'viewType': 'embed'} ) }} {% endfor %}
    {% endblock %} ``` # Render images > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). Render content images and configure their variations. To render images contained in Image Asset or Image fields, use the `ibexa_render_field()` Twig function. ```html+twig {{ ibexa_render_field(content, 'image') }} ``` You can pass the name of an [image variation](#configure-image-variation) as an argument, for example: ```html+twig {{ ibexa_render_field(content, 'image', { 'parameters': { 'alias': 'large' } }) }} ``` ## Render first image If a content item contains more than one image, you may want to select the first filled image to render. This enables you to avoid a situation where, for example, the featured image in an article is missing, because the first image field was left empty. The `ibexa_content_field_identifier_first_filled_image()` Twig function returns the identifier of the first image field that isn't empty. ```html+twig {% set firstImage = ibexa_content_field_identifier_first_filled_image(content) %} {{ ibexa_render_field(content, firstImage }} ``` > **Caution: Caution** > > This function works only for [Image](https://doc.ibexa.co/en/saas/content_management/field_types/field_type_reference/imagefield/index.md) fields. It doesn't work for [ImageAsset](https://doc.ibexa.co/en/saas/content_management/field_types/field_type_reference/imageassetfield/index.md) fields. ## Configure image variation The same image can have multiple variations differing in things such as scale, cropping, or applied filters. You can use the built-in image variations or [configure your own](https://doc.ibexa.co/en/saas/templating/image_variations/#custom-image-variations). The following example creates a custom variation that scales the image down to a 200 x 200 thumbnail and renders it in grayscale: ```yaml ibexa: system: site_group: image_variations: gray_thumb: reference: null filters: geometry/scaledownonly: [200, 200] colorspace/gray: [] ``` To use it, select the variation when rendering the image: ```html+twig {{ ibexa_render_field(content, 'image', { 'parameters': { 'alias': 'gray_thumb' } }) }} ``` ## Use focal point In the [image editor](https://doc.ibexa.co/en/saas/content_management/images/configure_image_editor/index.md) you can define a focal point for an image. The focal point doesn't have an instant effect when you use the default templates. However, you can use it to select the part of the image the view focuses on when the image is cropped. The following example shows how to use an image contained in an Image field as a focussed background. > **Note: Note** > > This implementation is only an example and depends on the JavaScript framework you're using. First, in the main template, render the Image field with a custom template: ```html+twig {{ ibexa_render_field(content, 'image', { 'template': 'fields/image.html.twig' }) }} ``` Then, create the custom Field template in `templates/fields/image.html.twig`, [overriding the default `ezimage_field` template block](https://doc.ibexa.co/en/saas/templating/render_content/render_content/#field-templates): ```html+twig {% block ezimage_field %} {% if field.value.additionalData.focalPointX is defined and field.value.additionalData.focalPointY is defined %} {% set position_x = (field.value.additionalData.focalPointX / field.value.width) * 100 %} {% set position_y = (field.value.additionalData.focalPointY / field.value.height) * 100 %} {% else %} {% set position_x = 50 %} {% set position_y = 50 %} {% endif %} {% set imageAlias = ibexa_image_alias( field, versionInfo, parameters.alias|default( 'original' ) ) %} {% set src = imageAlias ? asset( imageAlias.uri ) : "//:0" %}
    {% endblock %} ``` This template uses the focal point information contained in the image's additional data to position the background so that the focused part of the image is displayed. # Add breadcrumbs > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). Add and render a breadcrumbs element on your site. To add breadcrumbs to your website, first prepare a general layout template in a `templates/themes//pagelayout.html.twig` file. This template can contain things such as header, menu, footer, and [assets](https://doc.ibexa.co/en/saas/templating/assets/index.md) for the whole site, and all other templates [extend](https://doc.ibexa.co/en/saas/templating/templates/templates/#connecting-templates) it. Then, to render breadcrumbs, create a `BreadcrumbController.php` file in `src/Controller`: ```php query = new Criterion\Ancestor([$this->locationService->loadLocation($locationId)->pathString]); $results = $this->searchService->findLocations($query); $breadcrumbs = []; foreach ($results->searchHits as $searchHit) { $breadcrumbs[] = $searchHit; } return $this->render( '@ibexadesign/parts/breadcrumbs.html.twig', [ 'breadcrumbs' => $breadcrumbs, ] ); } } ``` The controller uses the [Ancestor Search Criterion](https://doc.ibexa.co/en/saas/search/criteria_reference/ancestor_criterion/index.md) to find all Ancestors of the current Location (line 27). It then places the ancestors in the `breadcrumbs` variable that you can use in the template. Next, call this controller from the page layout template and pass the current location ID as a parameter: ```html+twig {{ render( controller( "App\\Controller\\BreadcrumbController::showBreadcrumbsAction", { 'locationId': locationId, } ) ) }} ``` Finally, create a breadcrumb template in `templates/themes//parts/breadcrumbs.html.twig`, as indicated in the controller (line 34). In this template, iterate over all breadcrumbs and render links to them: ```html+twig {% for breadcrumb in breadcrumbs %} {% if not loop.first %} -> {% endif %} {% if not loop.last %} {{ breadcrumb.valueObject.contentInfo.name }} {% else %} {{ breadcrumb.valueObject.contentInfo.name }} {% endif %} {% endfor %} ``` # Add "forgot password" option > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). Add a "forgot password" form and configure templates for it. The "forgot password" option allows users of a specific SiteAccess, admin or front, to request a password change. You can customize the template used in the `/user/forgot-password` route. Follow the instructions to create and configure a "forgot password" form. Add the following configuration files under the `ibexa.system..user_forgot_password` [configuration key](https://doc.ibexa.co/en/saas/administration/configuration/configuration/#configuration-files): ```yaml ibexa: system: : user_forgot_password: templates: form: mail: ``` Under the `templates` key, provide the path to templates responsible for rendering the forgot password form (`form`) and email (`mail`), which users receive after they request a password change. The [default templates](https://github.com/ibexa/user/tree/6.0/src/bundle/Resources/views) for forgot password form and email are located in `ibexa/user/src/bundle/Resources/views`. The [templates](https://github.com/ibexa/admin-ui/tree/6.0/src/bundle/Resources/views/themes/admin/account/forgot_password) specific for the back office are in `ibexa/admin-ui/src/bundle/Resources/views/themes/admin/account`. You can also modify [other user management templates](https://doc.ibexa.co/en/saas/users/user_registration/#other-user-management-templates). To add a link redirecting to the reset password form, in the page layout template, provide the following code: ```html+twig {{ 'authentication.forgot_password'|trans|desc('Forgot password?') }} ``` You can customize the layout of templates according to your needs. For more information, see [Template documentation](https://doc.ibexa.co/en/saas/templating/templates/templates/index.md). # Add login form > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). Customize the login form for new users in your site front end. You can create a login form for your users. Follow the instruction below to create a template with login form. If you want to configure more options, for example, password expiration, see [other user management templates](https://doc.ibexa.co/en/saas/users/user_registration/#other-user-management-templates). First, make sure you have configured [login methods](https://doc.ibexa.co/en/saas/users/login_methods/index.md). If you only want to change a template, add the following configuration under the `ibexa.system..user` [configuration key](https://doc.ibexa.co/en/saas/administration/configuration/configuration/#configuration-files): ```yaml ibexa: system: my_siteaccess: user: login_template: '@ibexadesign/Security/login.html.twig' ``` To add a link redirecting to the login form, in the page layout template, provide the following code: ```html+twig Log in ``` Next, add the template defined in the event. In `templates/themes//login`, create an `expired_credentials.html.twig` file: ```html+twig {% extends '@ibexadesign/Security/base.html.twig' %} {%- block content -%}

    {{ 'authentication.credentials_expired.message'|trans|desc( 'For security reasons, your password has expired and needs to be changed. An email has been sent to you with instructions.' ) }}

    {%- endblock -%} ``` ## Customize login form You can use a custom template for example to display information about password expiration or to customize [other user management templates](https://doc.ibexa.co/en/saas/users/user_registration/#other-user-management-templates). In case of more advanced template customization, you can use a subscriber, for example in `src/EventSubscriber/LoginFormViewSubscriber.php`: ```php 'onPreContentView', ]; } public function onPreContentView(PreContentViewEvent $event): void { $view = $event->getContentView(); if (!($view instanceof LoginFormView)) { return; } $view->addParameters([ 'foo' => 'foo', 'bar' => 'bar', ]); if ($view->getLastAuthenticationException() instanceof CredentialsExpiredException) { // View with instruction to unlock account $view->setTemplateIdentifier('login/expired_credentials.html.twig'); } } } ``` In the provided example, in line 23, the `PRE_CONTENT_VIEW` event is used. You can also pass additional parameters to the view (line 35). In this case, at the instance of exception (line 40), the subscriber displays the `expired_credentials.html.twig` template (line 42). Remember to provide a template and point to it in the subscriber (in this case, in `templates/login/expired_credentials.html.twig`): ```html+twig {% extends '@ibexadesign/Security/base.html.twig' %} {%- block content -%}

    {{ 'authentication.credentials_expired.message'|trans|desc( 'For security reasons, your password has expired and needs to be changed. An email has been sent to you with instructions.' ) }}

    {%- endblock -%} ``` For more information, see [Templates documentation](https://doc.ibexa.co/en/saas/templating/templates/templates/index.md). # Add navigation menu > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). Enrich you site front with a menu displaying selected content items. To add a navigation menu to your website, prepare a general layout template in a `templates/themes//pagelayout.html.twig` file. This template can contain things such as header, menu, footer, and [assets](https://doc.ibexa.co/en/saas/templating/assets/index.md) for the whole site, and all other templates [extend](https://doc.ibexa.co/en/saas/templating/templates/templates/#connecting-templates) it. To select items that should be rendered in the menu, you can use one of the following ways: - create a [query](#render-menu-using-a-query) - create a [MenuBuilder](#create-a-menubuilder) ## Render menu using a query To create a menu that contains a specific set of content items, for example all content under the root location, use a [Query Type](https://doc.ibexa.co/en/saas/templating/queries_and_controllers/content_queries/index.md). First, in `src/QueryType`, create a custom `MenuQueryType.php` file that queries for all items that you want in the menu: ```php $criteria, 'sortClauses' => [ new SortClause\Location\Priority(LocationQuery::SORT_ASC), ], ]; return new LocationQuery($options); } public static function getName() { return 'Menu'; } public function getSupportedParameters() { return []; } } ``` In this case, it queries for all visible children of location `2`, the root location, (lines 15-16) and renders them in order according to their location priority. The Query Type has the name `Menu` (line 28). You can use it in the template to render the menu. Add the following `ibexa_render_content_query` function to the `pagelayout_html.twig` template: ```html+twig {{ ibexa_render_content_query({ 'query': { 'query_type': 'Menu', 'assign_results_to': 'menuItems' }, 'template': '@ibexadesign/pagelayout_menu.html.twig', }) }} ``` Next, add the `templates/themes//pagelayout_menu.html.twig` template, which renders the individual items of the menu: ```html+twig {% if menuItems is defined and menuItems is not empty %} {% for item in menuItems %}
  • {{ ibexa_content_name(item.valueObject.contentInfo) }}
  • {% endfor %} {% endif %} ``` ## Create a MenuBuilder To make a more configurable menu, where you select the specific items to render, use the [KNPMenuBundle](https://github.com/KnpLabs/KnpMenuBundle) that is installed together with the product. To use it, first create a `MenuBuilder.php` file in `src/Menu`: ```php factory->createItem('root'); $menu->addChild('Home', ['route' => 'ibexa.url.alias', 'routeParameters' => [ 'locationId' => 2, ]]); $menu->addChild('Blog', ['route' => 'ibexa.url.alias', 'routeParameters' => [ 'locationId' => 67, ]]); $menu->addChild('Search', ['route' => 'ibexa.search']); return $menu; } } ``` In the builder, you can define items that you want in the menu. For example, lines 21-23 add a specific location by using the `ibexa.url.alias` route. Line 27 adds a defined system route that leads to the search form. Next, register the menu builder as a service: ```yaml services: App\Menu\MenuBuilder: tags: - {name: knp_menu.menu_builder, method: buildMenu, alias: root} ``` Finally, you can render the menu in `pagelayout.html.twig`. Identify it by the name that you provided in the Menu Builder's `buildMenu()` method: ```html+twig {{ knp_menu_render('root') }} ``` # Add search form to front page > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). Add a search bar and customize the search form in your site front end. You can add a search form to selected parts of your front page and decide which parts of the form, such as filters, are rendered. This example shows how to add a basic search bar to the top of every page and to configure search form and result rendering. ## Add a search bar First, prepare a general layout template in a `templates/themes//pagelayout.html.twig` file, and include a search bar in this template: ```html+twig {% include '@ibexadesign/parts/search_bar.html.twig' %} {% block content %} {% endblock %} ``` Then, make sure that `pagelayout.html.twig` is included in your view configuration: ```yaml ibexa: system: site_group: page_layout: '@ibexadesign/pagelayout.html.twig' search_view: ``` The `parts/search_bar.html.twig` template uses the built-in `SearchController` to manage the search: ```html+twig ``` You can now go to the front page of your installation. An unstyled search bar appears at the top of the page. ## Customize search result page Search results are shown in the `/search` route. You can go directly to `/search` to view a full search page. Select the template that is used on this page with the following configuration: ```yaml ibexa: system: site_group: page_layout: '@ibexadesign/pagelayout.html.twig' search_view: full: default: template: "@ibexadesign/full/search.html.twig" match: [ ] ``` Now, add the `full/search.html.twig` template: ```html+twig {% block content %}
    {% include '@ibexadesign/parts/search_form.html.twig' with { form: form } %} {% if results is defined %}
    {{ 'search.header'|trans({'%total%': pager.nbResults})|desc('%total% search result(s):') }}
    {% if results is empty %}
    {{ 'search.no_result'|trans({'%query%': form.vars.value.query})|desc('No results found for "%query%".') }}
    {% else %}

    {{ 'search.name'|trans|desc('Name') }}

    {% if pager.haveToPaginate %}
    {{ pagerfanta(pager, '', {'pageParameter': '[search][page]'}) }}
    {% endif %} {% endif %} {% endif %}
    {% endblock %} ``` This template replaces the default table that displays search results with an unnumbered list. ## Render search form In the template above, line 5 includes a separate template for the search form. Create the `parts/search_form.html.twig` file: ```html+twig {{ form_start(form) }}
    {{ form_row(form.query) }}
    {{ form_end(form, {'render_rest': false}) }} ``` This template renders only a basic query field and a submit button. `'render_rest': false` ensures that the fields you don't explicitly add to the template aren't rendered (in this case, date selection, content type, and more). # AI # Artificial Intelligence > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). AI interactions with Cohesivo Cohesivo includes built-in AI capabilities. For example, it can provide recommendations to product customers and content readers with the [Raptor connector](https://doc.ibexa.co/en/saas/recommendations/raptor_integration/raptor_connector_guide/index.md), and assist editors in the back office with [AI Actions](https://doc.ibexa.co/en/saas/ai/ai_actions/ai_actions_guide/index.md). The platform is also open to external AI integrations through [MCP (Model Context Protocol) servers](https://doc.ibexa.co/en/saas/ai/mcp/mcp_guide/index.md), which allow AI agents to interact with the system in a standardized way. AI solutions are extensible. You can create [custom AI actions](https://doc.ibexa.co/en/saas/ai/ai_actions/extend_ai_actions/index.md) or expose [new MCP server capabilities](https://doc.ibexa.co/en/saas/ai/mcp/mcp_usage/index.md). AI integration goes even further: - Some AI agents can learn how to use the [REST](https://doc.ibexa.co/en/saas/api/rest_api/rest_api_usage/rest_api_usage/index.md) or [GraphQL](https://doc.ibexa.co/en/saas/api/graphql/graphql/index.md) APIs. - Other, like those integrated into IDEs, can learn how to use the [PHP API](https://doc.ibexa.co/en/saas/api/php_api/php_api/index.md) and assist you in code development. - [AI Actions](https://ez-systems-developer-documentation--3381.com.readthedocs.build/en/3381/ai/ai_actions/ai_actions/): AI Actions help editors by automating repetitive tasks. - [MCP Servers](https://ez-systems-developer-documentation--3381.com.readthedocs.build/en/3381/ai/mcp/mcp/): Overview of MCP resources in Cohesivo # AI Actions > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). AI Actions help editors by automating repetitive tasks. AI Actions enhance the usability and flexibility of Cohesivo by automating various tasks. After you configure it, it can generate alt text for images or transform text passages. You can also extend it to perform other tasks or support additional AI services. ## Getting Started - [AI Actions product guide](https://ez-systems-developer-documentation--3381.com.readthedocs.build/en/3381/ai/ai_actions/ai_actions_guide/): AI Actions help editors by automating repetitive tasks. - [Configure AI Actions](https://ez-systems-developer-documentation--3381.com.readthedocs.build/en/3381/ai/ai_actions/configure_ai_actions/): Configure AI Actions. - [Taxonomy suggestions](https://ez-systems-developer-documentation--3381.com.readthedocs.build/en/3381/content_management/taxonomy/taxonomy/#taxonomy-suggestions): Learn how to use AI to suggest tags and categories - [Policies](https://ez-systems-developer-documentation--3381.com.readthedocs.build/en/3381/permissions/policies/#ai-actions): Learn about the available AI Actions policies - [Work with AI Actions](https://doc.ibexa.co/projects/userguide/en/6.0/ai_actions/work_with_ai_actions/): Create new AI actions or modify existing ones to work faster and increase creativity. ## Development - [Extend AI Actions](https://ez-systems-developer-documentation--3381.com.readthedocs.build/en/3381/ai/ai_actions/extend_ai_actions/): Extend AI Actions by connecting to other services and adding new capabilities. - [AI Actions events](https://ez-systems-developer-documentation--3381.com.readthedocs.build/en/3381/api/event_reference/ai_action_events/): Events that are triggered when working with AI actions. - [REST API Reference](https://doc.ibexa.co/en/6.0/api/rest_api/rest_api_reference/rest_api_reference.html#tag/Connector-AI): See the available endpoints for AI Actions - [Action Configuration Search Criterion reference](https://ez-systems-developer-documentation--3381.com.readthedocs.build/en/3381/search/ai_actions_search_reference/action_configuration_criteria/): Search Criteria available for Action Configuration search - [Action Configuration Search Sort Clauses reference](https://ez-systems-developer-documentation--3381.com.readthedocs.build/en/3381/search/ai_actions_search_reference/action_configuration_sort_clauses/): Sort Clauses available for Action Configuration search - [Importing AI actions](https://ez-systems-developer-documentation--3381.com.readthedocs.build/en/3381/content_management/data_migration/importing_data/#ai-action-configurations): Learn how to manage Action Configurations using data migrations # AI Actions product guide > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). AI Actions help editors by automating repetitive tasks. ## What are AI Actions Wherever you look, artificial intelligence becomes more and more important by enhancing user interaction and automating complex processes. Cohesivo is equipped with the AI Actions feature, which harnesses AI's potential to automate time-consuming editorial tasks. AI Actions is an extensible solution for integrating features provided by AI services into your workflows, all managed through a user-friendly interface. Out-of-the-box, AI Actions solution includes two essential components: a framework package and an OpenAI connector package. The Anthropic and Gemini connectors are also available - as [LTS updates](https://doc.ibexa.co/en/saas/ibexa_products/editions/#lts-updates). AI Actions can integrate with [Ibexa Connect](https://doc.ibexa.co/projects/connect/en/latest/general/ibexa_connect/), to give you an opportunity to build complex data transformation workflows without having to rely on custom code. From the developer's perspective, the integration removes the burden of maintaining third-party AI handlers, and accelerates the deployment of AI-based solutions. AI Actions solution comes pre-configured with the following action types: - [Refine text](#refining-text): Rewrite existing text according to instructions set in a prompt - [Generate alternative text](#generating-alternative-text): Generate alt text for images for accessibility purposes - [Suggest taxonomy entries](#suggesting-taxonomy-entries): Generate tag or product category suggestions based on content fields ![AI Actions schematic](https://doc.ibexa.co/en/saas/ai/ai_actions/img/guide_ai_actions.png) You can extend the solution's capabilities beyond the default setup by creating custom connector modules, allowing users to take advantage of additional AI services, or customize the way data is processed and interpreted. For example, it could transform images, or generate illustrations for your articles based on their contents. The possibilities are endless and you're not limited to a specific AI service, avoiding vendor lock-in. ## Availability Ibexa Cloud is available in all Cohesivo editions. To begin using AI Actions, you must first [perform the initial configuration](https://doc.ibexa.co/en/saas/ai/ai_actions/configure_ai_actions/index.md). ### Prerequisites Connectors with external AI services delivered by Ibexa require that you first install them, and [configure other settings, such as an API key and billing method](https://doc.ibexa.co/en/saas/ai/ai_actions/configure_ai_actions/index.md). Integration with Ibexa Connect requires that you first [get the credentials](https://doc.ibexa.co/projects/connect/en/latest/general/ibexa_connect/#access-ibexa-connect) to your account, and the [API token](https://doc.ibexa.co/en/saas/ai/ai_actions/configure_ai_actions/#create-token). > **Note: Ibexa Connect Availability** > > Ibexa Connect comes with all contracts signed from 2023. If you signed your contract earlier, contact your customer success manager to use Ibexa Connect. ## How it works AI Actions rely on an extensible AI framework, which is responsible for gathering information from various sources, such as AI action types, AI action configurations, and contextual details like SiteAccess, user details, locale settings, and more. This data can then be combined with user input. It's then passed to a service connector, such as the default OpenAI connector or the Ibexa Connect connector, for final processing on Cohesivo side. The service connector wraps all data into a prompt or another suitable format and sends it to an external service. When the external service returns a response, the response goes back through the service connector and passes to the framework. It can then be presented to the user in any way necessary. ### Core concepts #### AI service AI service is a third party platform that provides access to artificial intelligence tools and capabilities. It executes tasks that it receives through a service connector. #### Action Actions are tasks or functions that are executed by an external AI service. Each action is a combination of an AI action type and an AI action configuration. Action types define what kind of task the AI service performs, while AI action configurations specify how the task should be executed. This clear separation allows for a flexible system where actions can be created, managed, and customized with minimal effort. #### AI action type AI action types are high level templates predefined by developers. AI action types correspond to tasks that users intend to perform when they interact with the interface. Each AI action type defines the structure and nature of the task that the AI service performs, and is interpreted by a handler. Action type definitions specify the following information: - an identifier - a set of input parameters - a set of output fields - a category of action, for example, "text to image", "video to text" AI action types could be designed, for example, to generate alternative text based on an image, translate a selected passage of text, or generate a video clip based on a description provided in the field. By defining AI action types, developers can create a wide range of functionalities that can be deployed within the application. #### AI action configuration AI action configurations store detailed parameters needed to generate AI actions based on AI action types. Website administrators manage AI action configurations in the [**Admin** panel](https://doc.ibexa.co/en/saas/administration/admin_panel/admin_panel/index.md), where they customize and fine-tune the behavior of each AI action. It might involve setting specific parameters used by the AI service, a response length, an expense limit, or configuring how the output should be handled. By making such adjustments, administrators can ensure that the actions are tailored to meet the needs of your organization. #### Model Once an AI action is defined and configured, it must be executed, and this is where models come into play. Each model is designed to work with a specific AI service and AI action type pair. Pieces of PHP code that are responsible for resolving a model are called handlers. They may include hardcoded prompts for conversational AI services like ChatGPT, or operate without prompts in the case of other types of AI. Handlers take parameters defined in the AI action type and configuration, combine it with user input and any predefined settings or prompts, and pass this information to the AI service for processing. ### Triggering actions from the UI Among other elements, AI Actions include UI components that are used in: - AI action management in the **Admin** panel - text modification in online editor - alt-text generation in the image management modal These areas are user-friendly and well integrated with the existing application’s UI. Administrators can manage action configurations with ease, while editors can trigger actions with a click of a button. Procedures are straightforward and intuitive, ensuring that users can quickly achieve their desired outcomes. ### Triggering actions programmatically AI Actions feature exposes a REST API interface that allows for programmatic execution of AI actions. With the API, developers can automate tasks and execute actions on batches of content by integrating them into workflows. For more information, see the [AI actions section in the REST API Reference](https://doc.ibexa.co/en/saas/api/rest_api/rest_api_reference/rest_api_reference.html#ai-actions-execute-ai-action). ## Capabilities ### Management Users with the appropriate permissions, governed by role-based [policies](https://doc.ibexa.co/en/saas/permissions/policies/#ai-actions), can control the lifecycle of AI actions by creating, editing, executing, and deleting them. Additionally, AI action configurations can be enabled or disabled depending on the organization's needs. ![Configurations management screen](https://doc.ibexa.co/en/saas/ai/ai_actions/img/ai_actions_list.png) An intuitive AI Actions interface within the **Admin** panel displays a list of all available AI actions. Here, you can search for specific actions and filter them by type or status. By accessing the detailed view of individual AI actions, you can quickly review all their parameters. ### Extensibility Built-in AI action types offer a good starting point, but the real power of AI Actions lies in extensibility. Extending AI Actions opens up new possibilities for content management and editing. Developers can define new models and AI action types that use the existing AI service or even integrate additional services. The latter involves developing a new service connector, writing a handler that communicates with the new service, defining a new AI action type, and creating a form for configuring options, which extends the default action configuration form shown in the **Admin** panel. For example, if this is your organization's requirement, a developer could write a handler that uses an AI service available internally, without exposing your data to a third-party service. ## Use cases Out of the box, after you configure access to the OpenAI service, AI Actions come with two action types that can help your organization with the following tasks. ### Refining text Content editors can benefit from using AI capabilities to [enhance or modify text](https://doc.ibexa.co/projects/userguide/en/6.0/content_management/create_edit_content_items/#ai-assistant). With a few clicks, they can improve content quality or reduce the workload. While working on content, editors can request that AI performs specific actions such as: adjusting the length of the text, changing the tone, or correcting linguistic errors. ![AI Assistant](https://doc.ibexa.co/en/saas/ai/ai_actions/img/ai_assistant.png) This functionality is available in content types that include RichText, Text line, Text Block fields, and certain Page Builder blocks. ### Generating alternative text Media managers and content editors can benefit from employing AI to [generate alt text for images](https://doc.ibexa.co/projects/userguide/en/6.0/image_management/upload_images/#ai), which results in improved accessibility and SEO. Once the feature is configured, editors can generate alt text for images they upload to the system by clicking one button. ![Alt text generation](https://doc.ibexa.co/en/saas/ai/ai_actions/img/alt_text_use_ai.png) With some customization, administrators could use the API to run a batch process against a larger collection of illustrations. ### Suggesting taxonomy entries Content editors and product managers can use [taxonomy suggestions](https://doc.ibexa.co/en/saas/content_management/taxonomy/taxonomy/#taxonomy-suggestions) when assigning tags or product categories to content items and products. Instead of manually browsing through extensive taxonomy trees, editors can request suggestions based on the content's text fields, such as name and description. > **Note: Alternative suggestion provider** > > By default, embeddings used by the taxonomy suggestions feature are generated with OpenAI. If you install and configure the [Google Gemini connector](https://doc.ibexa.co/en/saas/ai/ai_actions/configure_ai_actions/#install-google-gemini-connector), you can modify the [taxonomy suggestions settings](https://doc.ibexa.co/en/saas/content_management/taxonomy/taxonomy/#change-embeddings-provider-to-google-gemini) and use Google Gemini as an alternative embeddings provider. ### Performing advanced image to text analysis With some additional customization, store managers could benefit from automating part of product management by integrating their Cohesivo with Google Cloud Vision and the [product catalog](https://doc.ibexa.co/en/saas/product_catalog/product_catalog_guide/index.md) by using Ibexa Connect. Instead of manually selecting and linking images stored in a [DAM](https://doc.ibexa.co/en/saas/content_management/images/add_image_asset_from_dam/index.md) solution to their products, they could use of a no-code workflow where an AI service, for example, Google Cloud Vision, extracts text and attributes from product images, which are then matched with existing items in a product catalog. This would enable automatic product identification, tagging, and catalog updates, resulting in less manual work and more efficient product management. # Configure AI Actions > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). Configure AI Actions. AI Actions are available in Cohesivo regardless of its edition. To use this feature you must first configure the built-in service connectors or build your own ones. Once the framework is configured, before you can start using AI Actions, you can configure access to Ibexa-made service connectors by following the instructions below, or [create your own](https://doc.ibexa.co/en/saas/ai/ai_actions/extend_ai_actions/#create-custom-action-handler). Only then you can restart you application and start [working with the AI Actions feature](https://doc.ibexa.co/projects/userguide/en/6.0/ai_actions/work_with_ai_actions/). > **Note: Taxonomy suggestions** > > The default OpenAI or the optional Google Gemini connectors can used by the [Taxonomy suggestions](https://doc.ibexa.co/en/saas/content_management/taxonomy/taxonomy/#taxonomy-suggestions) feature to generate embeddings for suggesting tags and product categories. After you configure the OpenAI connector, or set up the optional Google Gemini connector and [modify the default taxonomy suggestions settings](https://doc.ibexa.co/en/saas/content_management/taxonomy/taxonomy/#change-embeddings-provider-to-google-gemini), you can [create AI actions that use the Text to Taxonomy action type](https://doc.ibexa.co/projects/userguide/en/6.0/ai_actions/work_with_ai_actions/#create-ai-actions-that-control-taxonomy-suggestions). You can also create [your own embedding provider](https://doc.ibexa.co/en/saas/content_management/taxonomy/taxonomy/#replace-the-embedding-provider). ## Configure access to OpenAI To use the built-in connector with the OpenAI service, you need to create an OpenAI account, [get an API key](https://help.openai.com/en/articles/4936850-where-do-i-find-my-openai-api-key), and make sure that you [set up a billing method](https://help.openai.com/en/articles/9038407-how-can-i-set-up-billing-for-my-account). Then, in the root folder of your project, modify the `.env` file: find the `OPENAI_API_KEY` variable and replace a placeholder value with the API key that you got from the AI service. ```bash ###> ibexa/connector-openai ### OPENAI_API_KEY= ###< ibexa/connector-openai ### ``` ### Sample OpenAI action configurations The AI actions come with sample AI action configurations to quickly get you started on using the feature. Based on these examples, which reflect the most common use cases, you can learn to configure your own AI actions with greater ease. ## Install Anthropic connector (LTS Update) Run the following command to install the package: ```bash composer require ibexa/connector-anthropic ``` If not using Symfony Flex, enable the bundle in `config/bundles.php`: ```php Ibexa\Bundle\ConnectorAnthropic\IbexaConnectorAnthropicBundle::class => ['all' => true], ``` This adds the feature code, including basic handlers that let you refine text or generate alternative text for images. To use the connector with the Anthropic services, you need to create an account, make sure that you [set up a billing method](https://support.claude.com/en/articles/8325618-paid-plan-billing-faqs), and get an API key. 1. Log in to your [Anthropic Claude console](https://platform.claude.com/login). 2. Go to **API keys** and click **Create Key**. 3. Select the workspace, enter a **Key Name** and click **Add**. 4. Take a note of the API key, because it is displayed only once. Then, in the root folder of your project, modify the `.env` file: add an `ANTHROPIC_API_KEY` variable and populate its value with the API key that you got from the AI service. ```bash ###> ibexa/connector-anthropic ### ANTHROPIC_API_KEY= ###< ibexa/connector-anthropic ### ``` By default, when reaching out for responses, the Anthropic connector uses the [Claude Sonnet 4](https://platform.claude.com/docs/en/about-claude/models/overview) model. Users can override this setting at runtime when they [edit or create an AI action](https://doc.ibexa.co/projects/userguide/en/6.0/ai_actions/work_with_ai_actions/#edit-existing-ai-actions). You can also change the default values globally. To do it, in `config/packages` folder, create a YAML file similar to this example: ```yaml ibexa_connector_anthropic: text_to_text: default_model: claude-sonnet-4-6 default_temperature: 0.8 default_max_tokens: 2045 models: claude-haiku-4-5-20251001: 'Claude Haiku 4.5 (fast, cost-efficient)' claude-sonnet-4-6: 'Claude Sonnet 4.6 (recommended)' claude-opus-4-6: 'Claude Opus 4.6 (advanced reasoning)' claude-opus-4-7: 'Claude Opus 4.7 (most capable)' ``` You can now use the Anthropic connector in your project. > **Note: Current model availability** > > Anthropic regularly releases new models and deprecates older ones. Before you configure the connector, check the [Anthropic models overview](https://platform.claude.com/docs/en/about-claude/models/overview) for the current list of supported model identifiers. ## Install Google Gemini connector (LTS Update) Run the following command to install the package: ```bash composer require ibexa/connector-gemini ``` Then, if not using Symfony Flex, enable the bundle in `config/bundles.php`: ```php return [ // ... Ibexa\Bundle\ConnectorGemini\IbexaConnectorGeminiBundle::class => ['all' => true], ]; ``` This adds the feature code, including basic handlers that let you refine text or generate alternative text for images. ### Get API key To use the connector with the Gemini services, you need to create an account, set up billing, enable Gemini API and get an API key. #### Create the Google Cloud project 1. Sign in to the [Google Cloud Console](https://console.cloud.google.com/). 2. In the top bar, click **Default Gemini Project** to open a project picker. 3. Click **New project** and provide project details: 1. Add project name, for example, "My project". 2. Modify the automatically generated **Project ID** if necessary. 3. Select location: choose your organization. 4. Click **Create**. #### Configure billing 1. Navigate to the Google Cloud Console's **Billing** page. 2. If you do not have one, click **Add billing account** and add a payment method. 3. In **Your projects** tab, locate your project, and in its line, from the **Actions** menu, select **Change billing**. 4. Select your active billing account, and click **Set account**. #### Enable the Gemini API 1. Navigate to the Google Cloud Console's **APIs & Services** page. 2. From the left-hand menu, select **Library** and search for the Generative Language API. 3. In the API's details page, click **Enable**. #### Generate the API key 1. Go to [Google AI Studio](https://aistudio.google.com/app/api-keys)'s **API keys** page, and click **Create API key**. 2. Provide a name for the API key, select "My project" from a list of projects and click **Create key**. 3. Back in the **API keys** list, in your project's line, copy the API key. ### Set API key in configuration Then, in the root folder of your project, modify the `.env` file: add an `GEMINI_API_KEY` variable and populate its value with the API key that you got from the AI service. ```bash ###> ibexa/connector-gemini ### GEMINI_API_KEY= ###< ibexa/connector-gemini ### ``` > **Note: Different API keys for different SiteAccesses** > > If there are multiple SiteAccesses in your installation, you can set different API keys for each SiteAccess. To do it, set the keys under the `ibexa.system.` [configuration key](https://doc.ibexa.co/en/saas/administration/configuration/configuration/#configuration-files), like so: > > ```yaml > ibexa: > system: > default: > connector_gemini: > gemini: > api_key: '%env(GEMINI_API_KEY)%' > base_url: 'https://generativelanguage.googleapis.com/v1beta/' # Google Gemini's API endpoint > ``` ### Configure default models By default, when reaching out for responses, the Gemini connector uses the Gemini Pro [model](https://ai.google.dev/gemini-api/docs/models) for text refinement and Gemini Flash model for alternative text generation. Users can override this setting at runtime when they [edit or create an AI action](https://doc.ibexa.co/projects/userguide/en/6.0/ai_actions/work_with_ai_actions/#edit-existing-ai-actions). You can also change the default values globally. To do it, in `config/packages` folder, create a YAML file similar to this example: ```yaml ibexa_connector_gemini: text_to_text: models: gemini-pro-latest: label: 'Gemini Pro Latest' max_tokens: 4096 gemini-flash-latest: label: 'Gemini Flash Latest' max_tokens: 4096 default_model: gemini-pro-latest default_max_tokens: 4096 # Must be <= the model’s max_tokens default_temperature: 0.8 image_to_text: models: gemini-flash-latest: label: 'Gemini Flash Latest' max_tokens: 4096 default_model: gemini-flash-latest default_max_tokens: 4096 default_temperature: 1.0 ``` When setting up models, make sure that you follow these rules: - `default_model` must reference a configured model - `default_max_tokens` must not exceed the model’s limit - If you use the same model for different action types, settings must be consistent > **Note: Google Gemini and taxonomy suggestions** > > To use Google Gemini for generating taxonomy suggestions, ensure that you [change the embeddings provider and model setting accordingly](https://doc.ibexa.co/en/saas/content_management/taxonomy/taxonomy/#change-embeddings-provider-to-google-gemini). You can now use the Gemini connector in your project. For more information, see [Extend Gemini connector](https://doc.ibexa.co/en/saas/ai/ai_actions/extend_ai_actions/#extend-google-gemini-connector). ## Configure access to Ibexa Connect First, get the credentials by contacting [Ibexa Support](https://support.ibexa.co). ### Create team In Ibexa Connect, set up the account, and [create a team](https://doc.ibexa.co/projects/connect/en/latest/access_management/teams/#creating-teams). Navigate to the team details page and note down the numerical value of the **Team id** variable. Creating a team matters, because [scenarios](https://doc.ibexa.co/projects/connect/en/latest/scenarios/creating_a_scenario/) that process data coming from your AI action are associated with a team. This way, if your organization has more than one Cohesivo project, each project can be linked to a different team and so can be scenarios used in those projects. If specific users from the team are supposed to modify scenario settings, you must [assign the right roles](https://doc.ibexa.co/projects/connect/en/latest/access_management/teams/#managing-teams) to them. ### Create token Navigate to your Ibexa Connect user's profile, and on the **API ACCESS** tab, create a new token. Select the following scopes to set permissions needed to enable the integration of platforms: - `custom-property-structures:read` - `custom-property-structures:write` - `hooks:read` - `hooks:write` - `scenarios:read` - `scenarios:write` - `team-variables:read` - `team-variables:write` - `teams:write` - `templates:read` - `templates:write` - `udts:read` - `udts:write` ![Creating an API token](https://doc.ibexa.co/en/saas/ai/ai_actions/img/connect_api_token.png) Copy the token code that appears on the tokens list, next to the label. ### Set up credentials In the root folder of your project, modify the `.env` file. Replace a placeholder value of the `IBEXA_CONNECT_TOKEN` variable with the token that you got from Ibexa Connect and provide a value of the `IBEXA_CONNECT_TEAM_ID` variable. ```bash ###> ibexa/connect ### IBEXA_CONNECT_HOST=https://connect.ibexa.co IBEXA_CONNECT_API_PATH=/api/v2/ # Token can be created in the user's profile in Ibexa Connect, under the 'API ACCESS' section. IBEXA_CONNECT_TOKEN= # Use the URL below to read more on Ibexa Connect teams. # https://doc.ibexa.co/projects/connect/en/latest/access_management/teams/ IBEXA_CONNECT_TEAM_ID=2 ###< ibexa/connect ### ``` ### Initiate integration Initiate the models provided by the handler by issuing the following command: ```bash php bin/console ibexa:connect:init-connect-ai ``` For example: ```bash php bin/console ibexa:connect:init-connect-ai 2 en connect-image-to-text connect-text-to-text ``` > **Note: Support for multiple Ibexa Connect languages** > > The [`language` attribute](https://developers.make.com/api-documentation/api-reference/templates#post-templates) determines the language in which template details such as module names will be displayed in Ibexa Connect's UI. Then, create the `Ibexa AI handler` custom property in Ibexa Connect to store the list of available action handlers for this integration. You can do it by running the following command: ```bash php bin/console ibexa:connect:init-custom-property-structures ``` For example: ```bash php bin/console ibexa:connect:init-custom-property-structures 4 connect-image-to-text connect-text-to-text ``` The `Ibexa AI handler` property attaches to a scenario to store information about the action handler associated with it. When creating a new Ibexa Connect-based AI action, the back office of Cohesivo shows only the existing scenarios that work with selected action handler. ### Customize templates Return to the Ibexa Connect dashboard and modify the **Template for connect...handler** [templates](https://doc.ibexa.co/projects/connect/en/latest/scenarios/scenario_templates/) by defining the logic needed to process the data. Once the templates are ready, you can build scenarios from them, either directly in Ibexa Connect or in [Cohesivo's user interface](https://doc.ibexa.co/projects/userguide/en/6.0/ai_actions/work_with_ai_actions/#create-new-ai-actions). # Extend AI Actions > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). Extend AI Actions by connecting to other services and adding new capabilities. By extending [AI Actions](https://doc.ibexa.co/en/saas/ai/ai_actions/ai_actions_guide/index.md), you can make regular content management and editing tasks more appealing and less demanding. You can start by integrating additional AI services to the existing action types or develop custom ones that impact completely new areas of application. For example, you can create a handler that connects to a translation model and use it to translate your website on-the-fly, or generate illustrations based on a body of an article. ## Execute Actions You can execute AI Actions by using the [ActionServiceInterface](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-ConnectorAi-ActionServiceInterface.html) service, as in the following example: ```php $action = new GenerateAltTextAction(new Image([$imageEncodedInBase64])); $action->setRuntimeContext(new RuntimeContext(['languageCode' => $languageCode])); $action->setActionContext( new ActionContext( new ActionConfigurationOptions(['default_locale_fallback' => 'en']), // System context new ActionConfigurationOptions(['max_lenght' => 100]), // Action Type options new ActionConfigurationOptions( // Action Handler options [ 'prompt' => 'Generate the alt text for this image in less than 100 characters.', 'temperature' => 0.7, 'max_tokens' => 4096, 'model' => 'gpt-4o-mini', ] ) ) ); $output = $this->actionService->execute($action)->getOutput(); ``` The `GenerateAltTextAction` is a built-in action that implements the [ActionInterface](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-ConnectorAi-ActionInterface.html), takes an [Image](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-ConnectorAi-Action-DataType-Image.html) as an input, and generates the alternative text in the response. This action is parameterized with the [RuntimeContext](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-ConnectorAi-Action-RuntimeContext.html) and the [ActionContext](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-ConnectorAi-Action-ActionContext.html), which allows you to pass additional options to the Action before it's executed. | Type of context | Type of options | Usage | Example | | --------------- | ---------------------- | ---------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------ | | Runtime Context | Runtime options | Sets additional parameters that are relevant to the specific action that is currently executed | Information about the language of the content that is being processed | | Action Context | Action Type options | Sets additional parameters for the Action Type | Information about the expected response length | | Action Context | Action Handler options | Sets additional parameters for the Action Handler | Information about the model, temperature, prompt, and max tokens allowed | | Action Context | System options | Sets additional information, not matching the other option collections | Information about the fallback locale | Both `ActionContext` and `RuntimeContext` are passed to the Action Handler (an object implementing the [ActionHandlerInterface](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-ConnectorAi-Action-ActionHandlerInterface.html)) to execute the action. The Action Handler is responsible for combining all the options together, sending them to the AI service and returning an [ActionResponse](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-ConnectorAi-ActionResponseInterface.html). You can pass the Action Handler directly to the `ActionServiceInterface::execute()` method, which overrides all the other ways of selecting the Action Handler. You can also specify the Action Handler by including it in the provided [Action Configuration](#action-configurations). In other cases, the Action Handler is selected automatically. You can affect this choice by creating your own class implementing the [ActionHandlerResolverInterface](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-ConnectorAi-Action-ActionHandlerResolverInterface.html) or by listening to the [ResolveActionHandlerEvent](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-ConnectorAi-Events-ResolveActionHandlerEvent.html) Event sent by the default implementation. You can influence the execution of an Action with two events: - [BeforeExecuteEvent](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-ConnectorAi-Action-Event-BeforeExecuteEvent.html), fired before the Action is executed - [ExecuteEvent](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-ConnectorAi-Action-Event-ExecuteEvent.html), fired after the Action is executed Below you can find the full example of a Symfony Command, together with a matching service definition. The command finds the images modified in the last 24 hours, and adds the alternative text to them if it's missing. ```php addArgument('user', InputArgument::OPTIONAL, 'Login of the user executing the actions', 'admin'); } protected function execute(InputInterface $input, OutputInterface $output): int { $this->setUser($input->getArgument('user')); $modifiedImages = $this->getModifiedImages(); $output->writeln(sprintf('Found %d modified image in the last 24h', $modifiedImages->getTotalCount())); /** @var \Ibexa\Contracts\Core\Repository\Values\Content\Content $content */ foreach ($modifiedImages as $content) { /** @var \Ibexa\Core\FieldType\Image\Value $value */ $value = $content->getFieldValue(self::IMAGE_FIELD_IDENTIFIER); if ($value === null || !$this->shouldGenerateAltText($value)) { $output->writeln(sprintf('Image %s has the image field empty, the file cannot be accessed, or the alternative text is already specified. Skipping.', $content->getName())); continue; } $contentUpdateStruct = $this->contentService->newContentUpdateStruct(); $value->alternativeText = $this->getSuggestedAltText($this->convertImageToBase64($value->uri), $content->getDefaultLanguageCode()); $contentUpdateStruct->setField(self::IMAGE_FIELD_IDENTIFIER, $value); $updatedContent = $this->contentService->updateContent( $this->contentService->createContentDraft($content->getContentInfo())->getVersionInfo(), $contentUpdateStruct ); $this->contentService->publishVersion($updatedContent->getVersionInfo()); } return Command::SUCCESS; } private function getSuggestedAltText(string $imageEncodedInBase64, string $languageCode): string { $action = new GenerateAltTextAction(new Image([$imageEncodedInBase64])); $action->setRuntimeContext(new RuntimeContext(['languageCode' => $languageCode])); $action->setActionContext( new ActionContext( new ActionConfigurationOptions(['default_locale_fallback' => 'en']), // System context new ActionConfigurationOptions(['max_lenght' => 100]), // Action Type options new ActionConfigurationOptions( // Action Handler options [ 'prompt' => 'Generate the alt text for this image in less than 100 characters.', 'temperature' => 0.7, 'max_tokens' => 4096, 'model' => 'gpt-4o-mini', ] ) ) ); $output = $this->actionService->execute($action)->getOutput(); assert($output instanceof Text); return $output->getText(); } private function convertImageToBase64(string $uri): string { $id = $this->binaryDataHandler->getIdFromUri($uri); $file = $this->binaryDataHandler->getContents($id); return 'data:image/jpeg;base64,' . base64_encode($file); } private function getModifiedImages(): ContentList { $filter = (new Filter()) ->withCriterion( new DateMetadata(DateMetadata::MODIFIED, Operator::GTE, strtotime('-1 day')) ) ->andWithCriterion(new ContentTypeIdentifier('image')); return $this->contentService->find($filter); } /** @phpstan-assert-if-true string $value->uri */ private function shouldGenerateAltText(Value $value): bool { return $this->fieldTypeService->getFieldType('ibexa_image')->isEmptyValue($value) === false && $value->isAlternativeTextEmpty() && $value->uri !== null; } private function setUser(string $userLogin): void { $this->permissionResolver->setCurrentUserReference($this->userService->loadUserByLogin($userLogin)); } } ``` ```yaml App\Command\AddMissingAltTextCommand: arguments: $binaryDataHandler: '@Ibexa\Core\IO\IOBinarydataHandler\SiteAccessDependentBinaryDataHandler' ``` Executing Actions this way has a major drawback: all the parameters are stored directly in the code and cannot be easily reused or changed. To manage configurations of an AI Action you need to use another concept: Action Configurations. ## Action Configurations ### Manage Action Configurations Action Configurations allow you to store the parameters for a given Action in the database and reuse them when needed. They can be managed [through the back office](https://doc.ibexa.co/projects/userguide/en/6.0/ai_actions/work_with_ai_actions/), [data migrations](https://doc.ibexa.co/en/saas/content_management/data_migration/importing_data/#ai-action-configurations), or through the PHP API. To manage Action Configurations through the PHP API, you need to use the [ActionConfigurationServiceInterface](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-ConnectorAi-ActionConfigurationServiceInterface.html) service. You can manage them using the following methods: - Creating them with `ActionConfigurationServiceInterface::createActionConfiguration()` by passing the [ActionConfigurationCreateStruct](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-ConnectorAi-ActionConfiguration-ActionConfigurationCreateStruct.html). - Updating them with `ActionConfigurationServiceInterface::updateActionConfiguration()` by passing the [ActionConfigurationUpdateStruct](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-ConnectorAi-ActionConfiguration-ActionConfigurationUpdateStruct.html). - Deleting them with `ActionConfigurationServiceInterface::deleteActionConfiguration()` by passing the [ActionConfigurationInterface](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-ConnectorAi-ActionConfigurationInterface.html). See the [AI Actions event reference](https://doc.ibexa.co/en/saas/api/event_reference/ai_action_events/#action-configurations-management) for a list of events related to these operations. You can get a specific Action Configuration using the `ActionConfigurationServiceInterface::getActionConfiguration()` method and search for them using the `ActionConfigurationServiceInterface::findActionConfigurations()` method. See [Action Configuration Search Criteria reference](https://doc.ibexa.co/en/saas/search/ai_actions_search_reference/action_configuration_criteria/index.md) and [Action Configuration Search Sort Clauses reference](https://doc.ibexa.co/en/saas/search/ai_actions_search_reference/action_configuration_sort_clauses/index.md) to discover query possibilities. The following example creates a new Action Configuration: ```php $refineTextActionType = $this->actionTypeRegistry->getActionType('refine_text'); $actionConfigurationCreateStruct = new ActionConfigurationCreateStruct('rewrite_casual'); $actionConfigurationCreateStruct->setType($refineTextActionType); $actionConfigurationCreateStruct->setName('eng-GB', 'Rewrite in casual tone'); $actionConfigurationCreateStruct->setDescription('eng-GB', 'Rewrites the text using a casual tone'); $actionConfigurationCreateStruct->setActionHandler('openai-text-to-text'); $actionConfigurationCreateStruct->setActionHandlerOptions(new ArrayMap([ 'max_tokens' => 4000, 'temperature' => 1, 'prompt' => 'Rewrite this content to improve readability. Preserve meaning and crucial information but use casual language accessible to a broader audience.', 'model' => 'gpt-4-turbo', ])); $actionConfigurationCreateStruct->setEnabled(true); $this->actionConfigurationService->createActionConfiguration($actionConfigurationCreateStruct); ``` Actions Configurations are tied to a specific Action Type and are translatable. ### Execute Actions with Action Configurations Reuse existing Action Configurations to simplify the execution of AI Actions. You can pass one directly to the `ActionServiceInterface::execute()` method: ```php $action = new RefineTextAction(new Text([ <<actionConfigurationService->getActionConfiguration('rewrite_casual'); $actionResponse = $this->actionService->execute($action, $actionConfiguration)->getOutput(); ``` The passed Action Configuration is only taken into account if the Action Context was not passed to the Action directly using the [ActionInterface::setActionContext()](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-ConnectorAi-ActionInterface.html#method_hasActionContext) method. The `ActionServiceInterface` service extracts the configuration options from the Action Configuration object and builds the Action Context object internally: - Action Type options are mapped to Action Type options in the Action Context - Action Handler options are mapped to Action Handler options in the Action Context - System Context options are modified using the [ContextEvent](https://doc.ibexa.co/en/saas/api/event_reference/ai_action_events/#others) event ## Create custom Action Handler Cohesivo comes with a built-in connector to OpenAI services, but you're not limited to it and can add support for additional AI services in your application. The following example adds a new Action Handler connecting to a local AI run using [the llamafile project](https://github.com/mozilla-ai/llamafile) which you can use to execute Text-To-Text Actions, such as the built-in "Refine Text" Action. When creating an Action Handler for Ibexa Connect, add the new handler identifier to the [`Ibexa AI handler` custom property](https://doc.ibexa.co/en/saas/ai/ai_actions/configure_ai_actions/#initiate-integration) in Ibexa Connect user interface. ### Register a custom Action Handler in the system Create a class implementing the [ActionHandlerInterface](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-ConnectorAi-Action-ActionHandlerInterface.html) and register it as a service: - The `ActionHandlerInterface::supports()` method decides whether the Action Handler is able to execute given Action. - The `ActionHandlerInterface::handle()` method is responsible for combining all the Action options together, sending them to the AI service and forming an Action Response. - The `ActionHandlerInterface::getIdentifier()` method returns the identifier of the Action Handler which you can use to refer to it in other places in the code. See the code sample below, together with a matching service definition: ```php getInput(); $text = $this->sanitizeInput($input->getText()); $systemMessage = $action->hasActionContext() ? $action->getActionContext()->getActionHandlerOptions()->get('system_prompt', '') : ''; $response = $this->client->request( 'POST', sprintf('%s/v1/chat/completions', $this->host), [ 'headers' => [ 'Authorization: Bearer no-key', ], 'json' => [ 'model' => 'LLaMA_CPP', 'messages' => [ (object)[ 'role' => 'system', 'content' => $systemMessage, ], (object)[ 'role' => 'user', 'content' => $text, ], ], 'temperature' => 0.7, ], ] ); $output = strip_tags((string) json_decode($response->getContent(), true)['choices'][0]['message']['content']); return new TextResponse(new Text([$output])); } public static function getIdentifier(): string { return self::IDENTIFIER; } private function sanitizeInput(string $text): string { return str_replace(["\n", "\r"], ' ', $text); } } ``` ```yaml App\AI\Handler\LLaVATextToTextActionHandler: tags: - { name: ibexa.ai.action.handler, priority: 0 } - { name: ibexa.ai.action.handler.text_to_text, priority: 0 } ``` The `ibexa.ai.action.handler` tag is used by the `ActionHandlerResolverInterface` to find all the Action Handlers in the system. The built-in Action Types use service tags to find Action Handlers capable of handling them and display in the back office UI: - Refine Text uses the `ibexa.ai.action.handler.text_to_text` service tag - Generate Alt Text uses the `ibexa.ai.action.handler.image_to_text` service tag ### Provide Form configuration Form configuration makes the Handler configurable by using the back office. The example handler uses the `system_prompt` option, which becomes part of the Action Configuration UI thanks to the following code: ```php add('system_prompt', TextareaType::class, [ 'required' => true, 'disabled' => $options['translation_mode'], 'label' => 'System message', ]); } public function configureOptions(OptionsResolver $resolver): void { $resolver->setDefaults([ 'translation_domain' => 'app_ai', 'translation_mode' => false, ]); $resolver->setAllowedTypes('translation_mode', 'bool'); } } ``` ```yaml app.connector_ai.action_configuration.handler.llava_text_to_text.form_mapper.options: class: Ibexa\Bundle\ConnectorAi\Form\FormMapper\ActionConfiguration\ActionHandlerOptionsFormMapper arguments: $formType: 'App\Form\Type\TextToTextOptionsType' tags: - name: ibexa.connector_ai.action_configuration.form_mapper.options type: !php/const \App\AI\Handler\LLaVaTextToTextActionHandler::IDENTIFIER ``` The created Form Type adds the `system_prompt` field to the Form. Use the `Ibexa\Bundle\ConnectorAi\Form\FormMapper\ActionConfiguration\ActionHandlerOptionsFormMapper` class together with the `ibexa.connector_ai.action_configuration.form_mapper.options` service tag to make it part of the Action Handler options form. Pass the Action Handler identifier (`LLaVATextToText`) as the type when tagging the service. The Action Handler and Action Type options are rendered in the back office using the built-in Twig options formatter. ![Custom Action Handler options rendered using the default Twig options formatter](https://doc.ibexa.co/en/saas/ai/ai_actions/img/action_handler_options.png "Custom Action Handler options rendered using the default Twig options formatter") You can create your own formatting by creating a class implementing the [OptionsFormatterInterface](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-ConnectorAi-ActionConfiguration-OptionsFormatterInterface.html) interface and aliasing it to `Ibexa\Contracts\ConnectorAi\ActionConfiguration\OptionsFormatterInterface`. The following service definition switches the options rendering to the other built-in options formatter, displaying the options as JSON. ```yaml Ibexa\Contracts\ConnectorAi\ActionConfiguration\OptionsFormatterInterface: alias: Ibexa\ConnectorAi\ActionConfiguration\JsonOptionsFormatter ``` ## Custom Action Type use case With custom Action Types you can create your own tasks for the AI services to perform. They can be integrated with the rest of the AI framework provided by Ibexa and incorporated into the back office. The following example shows how to implement a custom Action Type dedicated for transcribing audio with an example Handler using [the OpenAI's Whisper](https://github.com/openai/whisper) project. ### Create custom Action Type Start by creating your own Action Type, a class implementing the [ActionTypeInterface](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-ConnectorAi-ActionType-ActionTypeInterface.html). The class needs to define following parameters of the Action Type: - name - identifier - input type identifier - output type identifier - Action object ```php $actionHandlers*/ public function __construct(private iterable $actionHandlers) { } public function getIdentifier(): string { return self::IDENTIFIER; } public function getName(): string { return 'Transcribe audio'; } public function getInputIdentifier(): string { return Audio::getIdentifier(); } public function getOutputIdentifier(): string { return Text::getIdentifier(); } public function getOptions(): array { return []; } public function createAction(DataType $input, array $parameters = []): ActionInterface { if (!$input instanceof Audio) { throw new InvalidArgumentException( 'audio', 'expected \App\AI\DataType\Audio type, ' . get_debug_type($input) . ' given.' ); } return new TranscribeAudioAction($input); } public function getActionHandlers(): iterable { return $this->actionHandlers; } } ``` ```yaml App\AI\ActionType\TranscribeAudioActionType: arguments: $actionHandlers: !tagged_iterator tag: app.connector_ai.action.handler.audio_to_text default_index_method: getIdentifier index_by: key tags: - { name: ibexa.ai.action.type, identifier: !php/const \App\AI\ActionType\TranscribeAudioActionType::IDENTIFIER } ``` The service definition introduces a custom `app.connector_ai.action.handler.audio_to_text` service tag to mark all the handlers capable of working with this Action Type. The `ibexa.ai.action.type` service tag registers the class in the service container as a new Action Type. If the Action Type is meant to be used mainly with prompt-based systems you can use the [LLMBaseActionTypeInterface](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-ConnectorAi-Action-LLMBaseActionTypeInterface.html) interface as the base for your Action Type. It allows you to define a base prompt directly in the Action Type that can be common for all Action Configurations. Action Type names can be localized using the Translation component. See the built-in Action Types like Generate Alt Text or Refine Text for an example. ### Create custom Data classes The `TranscribeAudio` Action Type requires adding two data classes that exist in its definition: - an `Audio` class, implementing the [DataType interface](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-ConnectorAi-DataType.html), to store the input data for the Action ```php */ final class Audio implements DataType { /** * @param non-empty-array $base64 */ public function __construct(private array $base64) { } public function getBase64(): string { return reset($this->base64); } public function getList(): array { return $this->base64; } public static function getIdentifier(): string { return 'audio'; } } ``` - an `TranscribeAudioAction` class, implementing the [ActionInterface interface](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-ConnectorAi-ActionInterface.html). Pass this object to the `ActionServiceInterface::execute()` method to execute the action. ```php audio; } public function getActionTypeIdentifier(): string { return 'transcribe_audio'; } } ``` ### Create custom Action Type options form Custom Form Type is needed if the Action Type requires additional options configurable in the UI. The following example adds a checkbox field that indicates to the Action Handler whether the transcription should include the timestamps. ```php add('include_timestamps', CheckboxType::class, [ 'required' => false, 'disabled' => $options['translation_mode'], 'label' => 'Include timestamps', ]); } public function configureOptions(OptionsResolver $resolver): void { $resolver->setDefaults([ 'translation_domain' => 'app_ai', 'translation_mode' => false, ]); $resolver->setAllowedTypes('translation_mode', 'bool'); } } ``` ```yaml app.connector_ai.action_configuration.handler.transcribe_audio.form_mapper.options: class: Ibexa\Bundle\ConnectorAi\Form\FormMapper\ActionConfiguration\ActionTypeOptionsFormMapper arguments: $formType: 'App\Form\Type\TranscribeAudioOptionsType' tags: - name: ibexa.connector_ai.action_configuration.form_mapper.action_type_options type: !php/const \App\AI\ActionType\TranscribeAudioActionType::IDENTIFIER ``` The built-in `Ibexa\Bundle\ConnectorAi\Form\FormMapper\ActionConfiguration\ActionTypeOptionsFormMapper` renders the Form Type in the back office when editing the Action Configuration for a specific Action Type (indicated by the `type` attribute of the `ibexa.connector_ai.action_configuration.form_mapper.action_type_options` service tag). ### Create custom Action Handler An example Action Handler combines the input data and the Action Type options and passes them to the Whisper executable to form an Action Response. The language of the transcribed data is extracted from the Runtime Context for better results. The Action Type options provided in the Action Context dictate whether the timestamps will be removed before returning the result. ```php \d{2}:\d{2}\.\d{3}]\s*/'; public function supports(ActionInterface $action): bool { return $action->getActionTypeIdentifier() === TranscribeAudioActionType::IDENTIFIER; } public function handle(ActionInterface $action, array $context = []): ActionResponseInterface { /** @var \App\AI\DataType\Audio $input */ $input = $action->getInput(); $path = $this->saveInputToFile($input->getBase64()); $arguments = ['whisper']; $language = $action->getRuntimeContext()?->get('languageCode'); if ($language !== null) { $arguments[] = sprintf('--language=%s', substr((string) $language, 0, 2)); } $arguments[] = '--output_format=txt'; $arguments[] = $path; $process = new Process($arguments); $process->run(); if (!$process->isSuccessful()) { unlink($path); throw new ProcessFailedException($process); } $output = $process->getOutput(); $includeTimestamps = $action->getActionContext() ?->getActionTypeOptions() ->get('include_timestamps', false) ?? false; if (!$includeTimestamps) { $output = $this->removeTimestamps($output); } unlink($path); return new TextResponse(new Text([$output])); } public static function getIdentifier(): string { return 'whisper_audio_to_text'; } private function removeTimestamps(string $text): string { $lines = explode(PHP_EOL, $text); $processedLines = array_map(static fn (string $line): string => preg_replace(self::TIMESTAMP_FORMAT, '', (string) $line) ?? '', $lines); return implode(PHP_EOL, $processedLines); } private function saveInputToFile(string $audioEncodedInBase64): string { $filename = uniqid('audio'); $path = sys_get_temp_dir() . \DIRECTORY_SEPARATOR . $filename; file_put_contents($path, base64_decode($audioEncodedInBase64)); return $path; } } ``` ```yaml App\AI\Handler\WhisperAudioToTextActionHandler: tags: - { name: ibexa.ai.action.handler, priority: 0 } - { name: app.connector_ai.action.handler.audio_to_text, priority: 0 } ``` ### Integrate with the REST API At this point the custom Action Type can already be executed by using the PHP API. To integrate it with the [AI Actions execute endpoint](https://doc.ibexa.co/en/saas/api/rest_api/rest_api_reference/rest_api_reference.html#ai-actions-execute-ai-action) you need to create additional classes responsible for parsing the request and response data. See [adding custom media type](https://doc.ibexa.co/en/saas/api/rest_api/extending_rest_api/adding_custom_media_type/index.md) and [creating new REST resource](https://doc.ibexa.co/en/saas/api/rest_api/extending_rest_api/creating_new_rest_resource/index.md) to learn more about extending the REST API. #### Handle input data Start by creating an Input Parser able to handle the `application/vnd.ibexa.api.ai.TranscribeAudio` media type. ```php $data */ public function parse(array $data, ParsingDispatcher $parsingDispatcher): TranscribeAudioAction { $this->assertInputIsValid($data); $runtimeContext = $this->getRuntimeContext($data); return new TranscribeAudioAction( new AudioDataType([$data[self::AUDIO_KEY][self::BASE64_KEY]]), $runtimeContext ); } /** @param array $data */ private function assertInputIsValid(array $data): void { if (!array_key_exists(self::AUDIO_KEY, $data)) { throw new \InvalidArgumentException('Missing audio key'); } if (!array_key_exists(self::BASE64_KEY, $data[self::AUDIO_KEY])) { throw new \InvalidArgumentException('Missing base64 key'); } } /** * @param array $data */ private function getRuntimeContext(array $data): RuntimeContext { return new RuntimeContext( $data[Action::RUNTIME_CONTEXT_KEY] ?? [] ); } } ``` ```yaml App\AI\REST\Input\Parser\TranscribeAudio: parent: Ibexa\Rest\Server\Common\Parser tags: - { name: ibexa.rest.input.parser, mediaType: application/vnd.ibexa.api.ai.TranscribeAudio } ``` The `TranscribeAudioAction` is a value object holding the parsed request data. ```php input; } public function getRuntimeContext(): RuntimeContext { return $this->runtimeContext; } } ``` #### Handle output data To transform the `TranscribeAudioAction` into a REST response you need to create: - An `AudioText` value object holding the REST response data ```php getOutput() ); } } ``` ```yaml App\AI\REST\Output\Resolver\AudioTextResolver: tags: - { name: ibexa.ai.action.mime_type, key: application/vnd.ibexa.api.ai.AudioText } ``` - A visitor converting the response value object into a serialized REST response: ```php getOutput(); $generator->startObjectElement(self::OBJECT_IDENTIFIER, $mediaType); $visitor->setHeader('Content-Type', $generator->getMediaType($mediaType)); $visitor->visitValueObject($text); $generator->endObjectElement(self::OBJECT_IDENTIFIER); } } ``` ```yaml App\AI\REST\Output\ValueObjectVisitor\AudioText: parent: Ibexa\Contracts\Rest\Output\ValueObjectVisitor tags: - { name: ibexa.rest.output.value_object.visitor, type: App\AI\REST\Value\AudioText } ``` You can now execute a specific Action Configuration for the new custom Action Type through REST API by sending the following request: ```http POST /ai/action/execute/my_action_configuration HTTP/1.1 Accept: application/vnd.ibexa.api.ai.AudioText+json Content-Type: application/vnd.ibexa.api.ai.TranscribeAudio+json ``` ```json { "TranscribeAudio": { "Audio": { "base64": "audioEncodedInBase64" }, "RuntimeContext": { "languageCode": "eng-GB" } } } ``` ### Integrate into the back office The last step in fully integrating the Transcribe Audio Action Type embeds it directly into the back office, allowing Editors to invoke it while doing their daily work. Extend the default editing template of the `ibexa_binaryfile` fieldtype by creating a new file called `templates/themes/admin/admin/ui/fieldtype/edit/form_fields_binary_ai.html.twig`. This template embeds the AI component, but only if a dedicated `transcript` field (of `ibexa_text` type) is available in the same content type to store the content of the transcription. ```twig {% extends '@ibexadesign/ui/field_type/edit/ibexa_binaryfile.html.twig' %} {% block ibexa_binaryfile_preview %} {{ parent() }} {% import '@ibexadesign/connector_ai/ui/ai_module/macros.html.twig' as ai_macros %} {% set transcriptFieldIdentifier = 'transcript' %} {% set fieldTypeIdentifiers = form.parent.parent.vars.value|keys %} {% if transcriptFieldIdentifier in fieldTypeIdentifiers %} {% set use_ai_btn_attr = { class: 'btn ibexa-btn ibexa-btn--secondary ibexa-ai-component--custom-btn', module_id: 'TranscribeAudio', scroll_selector: '.ibexa-edit-content', container_selector: '.ibexa-edit-content', input_selector: '.ibexa-field-edit-preview__action--preview', output_selector: '#ezplatform_content_forms_content_edit_fieldsData_transcript_value', ai_config_id: 'transcribe_audio', } %} {% endif %} {% endblock %} ``` And add it to the SiteAccess configuration for the `admin_group`: ```yaml ibexa: system: admin_group: admin_ui_forms: content_edit: form_templates: - { template: '@ibexadesign/admin/ui/fieldtype/edit/form_fields_binary_ai.html.twig', priority: -10 } ``` The configuration of the AI component takes the following parameters: - `module_id` - name of the JavaScript module to handle the invoked action. `ImgToText` is a built-in one handling alternative text use case, `TranscribeAudio` is a custom one. - `ai_config_id` - identifier of the Action Type to load Action Configurations for. The `ibexa_ai_config` Twig function is used under the hood. - `container_selector` - CSS selector to narrow down the HTML area which is affected by the AI component. - `input_selector` - CSS selector indicating the input field (must be below the `container_selector` in the HTML structure). - `output_selector` - CSS selector indicating the output field (must be below the `container_selector` in the HTML structure). - `cancel_wrapper_selector` - CSS selector indicating the element to which the "Cancel AI" UI element is attached. Now create the JavaScript module mentioned in the template that is responsible for: - gathering the input data (downloading the attached binary file and converting it into base64) - executing the Action Configuration chosen by the editor through the REST API - attaching the response to the output field You can find the code of the module below. Place it in a file called `assets/js/transcribe.audio.js` ```js import BaseAIAssistantComponent from '@ibexa-connector-ai/src/bundle/Resources/public/js/core/base.ai.assistant.component'; import Textarea from '@ibexa-connector-ai-modules/ai-assistant/fields/textarea/textarea'; export default class TranscribeAudio extends BaseAIAssistantComponent { constructor(mainElement, extraConfig) { super(mainElement, extraConfig); this.requestHeaders = { Accept: 'application/vnd.ibexa.api.ai.AudioText+json', 'Content-Type': 'application/vnd.ibexa.api.ai.TranscribeAudio+json', }; this.getRequestBody = this.getRequestBody.bind(this); this.getResponseValue = this.getResponseValue.bind(this); this.replacedField = Textarea; } getAudioInBase64() { const request = new XMLHttpRequest(); request.open('GET', this.inputElement.href, false); request.overrideMimeType('text/plain; charset=x-user-defined'); request.send(); if (request.status === 200) { return this.convertToBase64(request.responseText); } } getRequestBody() { const inputValue = this.getInputValue(); const body = { TranscribeAudio: { Audio: { base64: inputValue, }, RuntimeContext: {}, }, }; if (this.languageCode) { body.TranscribeAudio.RuntimeContext.languageCode = this.languageCode; } return JSON.stringify(body); } convertToBase64(data) { let binary = ''; for (let i = 0; i < data.length; i++) { binary += String.fromCharCode(data.charCodeAt(i) & 0xff); } return btoa(binary); } getResponseValue(response) { return response.AudioText.Text.text[0]; } handleAIDialogConfirm(responseText) { this.outputElement.value = responseText; this.outputElement.dispatchEvent(new Event('input')); super.handleAIDialogClose(responseText); } } ``` The last step is adding the module to the list of AI modules in the system, by using the provided `addModule` function. Create a file called `assets/js/addAudioModule.js`: ```js import { addModule } from '@ibexa-connector-ai/src/bundle/Resources/public/js/core/create.ai.module'; import TranscribeAudio from './transcribe.audio'; addModule(TranscribeAudio); ``` And include it into the back office using Webpack Encore. See [configuring assets from main project files](https://doc.ibexa.co/en/saas/administration/back_office/back_office_elements/importing_assets_from_bundle/#configuration-from-main-project-files) to learn more about this mechanism. ```js const ibexaConfigManager = require('./ibexa.webpack.config.manager.js'); ibexaConfigManager.add({ ibexaConfig, entryName: 'ibexa-admin-ui-layout-js', newItems: [ path.resolve(__dirname, './assets/js/addAudioModule.js') ], }); ``` Your custom Action Type is now fully integrated into the back office UI and can be used by the Editors. ![Transcribe Audio Action Type integrated into the back office](https://doc.ibexa.co/en/saas/ai/ai_actions/img/transcribe_audio.png "Transcribe Audio Action Type integrated into the back office") ## Extend Google Gemini connector (LTS Update) The Gemini connector provides several extension points that allow you to customize available models, behavior, validation, and response handling, while remaining compatible with the AI Actions framework. The connector builds Gemini requests in an options provider and formats responses through a response formatter. Both components can be replaced or extended to customize how requests are constructed and how responses are normalized. ### Add or customize models You can register additional Gemini models or customize existing ones by extending the connector’s model [configuration](https://doc.ibexa.co/en/saas/ai/ai_actions/configure_ai_actions/#configure-default-models). Extend the models map by defining: - a human-readable label - a `max_tokens` limit Optionally, you can set the default model that would be used for the action type that you're modifying, the default allowed tokens limit and the default temperature. Default values must stay within the limits supported by the [Gemini API](https://ai.google.dev/gemini-api/docs/models). ### Add a custom Action Handler To introduce a new Gemini-based AI action: 1. Create a handler that extends `Ibexa\Contracts\ConnectorAi\Action\AbstractActionHandler`. 2. Register the handler in `services/ai_action_handlers.yaml`. 3. Provide supporting components as needed: - a prompt factory - a form type for configuration - validators for action options This follows the same extension mechanism as other [custom AI actions](#create-custom-action-handler). ### Add custom response formatting To change how Gemini responses are post-processed or normalized: 1. Implement the `Ibexa\ConnectorGemini\Response\GeminiResponseFormatterInterface` interface. 2. Alias your implementation in the service container to override the default formatter. ### Add custom validation Add extra validation rules for Gemini action configuration options by tagging custom validators: - For `text-to-text` actions: ```yaml ibexa.connector_ai.action_configuration.options.validator.gemini_text_to_text ``` - For `image-to-text` actions: ```yaml ibexa.connector_ai.action_configuration.options.validator.gemini_image_to_text ``` ### Replace the Gemini client implementation To get full control over the low-level API communication without modifying the connector itself, you can swap the Gemini client implementation entirely with your own: - Use dependency injection to bind your own implementation to `Ibexa\ConnectorGemini\Client\GeminiClientInterface`. # MCP Servers > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). Overview of MCP resources in Cohesivo Editions: LTS Update The Model Context Protocol (MCP) and MCP Servers allow AI agents to interact with the system in a structured way. The feature is available as an [LTS Update](https://doc.ibexa.co/en/saas/ibexa_products/editions/#lts-updates) since v5.0.8. - [MCP Servers product guide](https://ez-systems-developer-documentation--3381.com.readthedocs.build/en/3381/ai/mcp/mcp_guide/): MCP servers expose tools, specialized prompts, and resources to AI agents. - [Install and configure MCP Servers](https://ez-systems-developer-documentation--3381.com.readthedocs.build/en/3381/ai/mcp/mcp_config/): Configure an MCP server that exposes built-in and custom tools, prompts, and resources. - [Work with MCP servers](https://ez-systems-developer-documentation--3381.com.readthedocs.build/en/3381/ai/mcp/mcp_usage/): Create custom capabilities for your MCP servers and test them. # MCP Servers product guide > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). MCP servers expose tools, specialized prompts, and resources to AI agents. Editions: LTS Update ## What is MCP Servers MCP ([Model Context Protocol](https://modelcontextprotocol.io/docs/2025-11-25/getting-started/intro)) is a protocol that standardizes how AI systems interact with external systems. While [AI actions](https://doc.ibexa.co/en/saas/ai/ai_actions/ai_actions_guide/index.md) integrate AI with the back office, Cohesivo's [MCP Servers](https://modelcontextprotocol.io/docs/2025-11-25/learn/server-concepts) offer an API that can be used by AI agents from the outside of the system. Because MCP is a standard protocol, many agents are already trained to use it. They can interact directly with REST or GraphQL APIs if their users provide detailed instructions through prompts, skill files, etc. However, when facing a specific REST or GraphQL API, an agent may misunderstand the purpose of endpoints, hallucinate paths, or send incorrectly structured parameters. MCP servers make the discovery of available capabilities much easier. They help AI agents translate natural language prompts into concrete actions on the system. ![MCP communication diagram showing AI agent client connecting to MCP server within Cohesivo.](https://doc.ibexa.co/en/saas/ai/mcp/img/mcp-com-diagram.png) An MCP server allows the agent to discover available tools, inspect their parameters, learn how to use them, and select the correct action. ## Availability MCP Servers feature is an [LTS Update package](https://doc.ibexa.co/en/saas/ibexa_products/editions/#lts-updates) available starting with the v5.0.8 in all Cohesivo editions. ## Capabilities With the MCP Servers feature, you can: - create MCP servers [by using YAML configuration](https://doc.ibexa.co/en/saas/ai/mcp/mcp_config/#mcp-server-configuration) - assign different tools, prompts, and resources to different MCP servers, varying them for each site and purpose - use [built-in tools](https://doc.ibexa.co/en/saas/ai/mcp/mcp_config/#built-in-tools) included in the package - [create custom server capabilities](https://doc.ibexa.co/en/saas/ai/mcp/mcp_usage/#create-capability-class) with PHP API MCP servers are defined specifically for each [repository](https://doc.ibexa.co/en/saas/administration/configuration/repository_configuration/index.md) and assigned to individual [SiteAccesses](https://doc.ibexa.co/en/saas/multisite/siteaccess/siteaccess/index.md) scopes. This way you can build flexible configurations that match different contexts. # Install and configure MCP Servers > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). Configure an MCP server that exposes built-in and custom tools, prompts, and resources. Editions: LTS Update With Cohesivo's MCP Servers LTS Update package, you can expose [MCP servers](https://doc.ibexa.co/en/saas/ai/mcp/mcp_guide/index.md) to external AI agents. ## Installation Run the following command to install the package: ```bash composer require ibexa/mcp ``` MCP Servers feature comes with [built-in tools](#built-in-tools) but doesn't come with a default configuration. You have to create your own MCP servers by providing [their configuration](#mcp-server-configuration) and [enable JWT authentication for them](#jwt-mcp-firewall). ## Configure authentication ### JWT MCP firewall AI agents use JWT authentication against Cohesivo's MCP servers. In `config/packages/lexik_jwt_authentication.yaml`, [enable the `authorization_header` token extractor](https://doc.ibexa.co/en/saas/infrastructure_and_maintenance/security/development_security/#jwt-authentication) to allow the use of JWT token bearer in `Authorization` header. In `config/packages/security.yaml`, make the following changes: - Uncomment the `ibexa_jwt_rest` firewall to enable requesting JWT tokens through REST or GraphQL API. - Add the `ibexa_jwt_mcp` firewall to allow the use of JWT authentication against MCP servers. ```yaml security: firewalls: # … ibexa_jwt_mcp: request_matcher: Ibexa\Mcp\Security\McpRequestMatcher user_checker: Ibexa\Core\MVC\Symfony\Security\UserChecker provider: ibexa stateless: true jwt: ~ ``` > **Note: Authentication for the APIs** > > You don't need to activate JWT authentication for the REST or GraphQL API. > > For sample JWT token requests, see [REST JWT authentication](https://doc.ibexa.co/en/saas/api/rest_api/rest_api_authentication/#jwt-authentication), [GraphQL JWT authentication](https://doc.ibexa.co/en/saas/api/graphql/graphql/#jwt-authentication) and [cURL test of MCP server](https://doc.ibexa.co/en/saas/ai/mcp/mcp_usage/#perform-curl-test). ### Repository user The AI agents authenticate against the MCP server with a JWT token generated for a specific repository user account. This repository user can be: - an individual user account (for example, of an editor or administrator) - a dedicated account created specifically for AI integrations The repository user can generate a JWT token with their own account, or a secondary dedicated account, and pass the token to the MCP client. A gateway could use a dedicated shared repository user to generate a JWT token and establish the connection. ## MCP server configuration You define MCP servers within a repository configuration and then assign those servers to specific SiteAccess scopes. ```yaml ibexa: repositories: : mcp: : path: enabled: true # Server options… discovery_cache: session: type: # Session options… allowed_hosts: - '' system: : mcp: servers: - ``` Servers are automatically registered as services with an ID following the pattern `ibexa.mcp.server..`. You can list all defined servers by running the following command: ```bash php bin/console debug:container ibexa.mcp.server ``` Routes are built automatically from MCP server `path` configs. Those routes are identified as `ibexa.mcp.`. You can list them by running the following command: ```bash php bin/console debug:router --siteaccess= ibexa.mcp` ``` ### MCP server options | Option | Type | Required | Default | Description | | --------------------------------------------------------------------------------------------------------------- | ------- | -------- | ----------------------------------------------- | ---------------------------------------------------------------- | | `path` | string | Yes | | MCP server endpoint path (appended to SiteAccess-aware base URL) | | `enabled` | boolean | No | `false` | Server state: decides whether it is enabled or disabled | | `version` | string | No | `1.0.0` | MCP server version | | [`description`](https://modelcontextprotocol.io/specification/2025-11-25/schema#implementation-description) | string | No | `null` | Server implementation description | | [`instructions`](https://modelcontextprotocol.io/specification/2025-11-25/schema#initializeresult-instructions) | string | No | `null` | Prompt-like instructions provided to the AI agent | | [`tools`](#tool-configuration) | array | No | `[]` | List of tool classes | | [`discovery_cache`](#discovery-cache) | string | Yes | | PSR-6 or PSR-16 cache pool service identifier | | [`session`](#session-storage) | object | No | `{ type: psr16,` `service: ibexa.cache_pool }` | Session storage configuration | | [`allowed_hosts`](#allowed-hosts) | array | No | `[` `'localhost',` `'127.0.0.1',` `'[::1]'` `]` | Accepted `Host` headers | > **Note: New servers are disabled by default** > > After you define a server, it remains disabled until you explicitly enable it. ### Tool configuration The main capabilities of an MCP server are called [tools](https://modelcontextprotocol.io/specification/2025-11-25/server/tools). They are the actions that an AI agent can invoke on the system. > **Note: MCP server design best practices** > > Avoid creating MCP servers with large tool sets. Too many tools make it more difficult for the AI agent to select the appropriate action. Instead, create multiple MCP servers with specific sets of tools dedicated to specific contexts or use cases. When designing MCP servers, focus on the needs and tasks of the human user who actually interacts with the AI agent rather than exploring every technical capability. There are two ways to associate tools with a server: - By listing PHP classes (FQCNs) in the server's configuration `tools`. All tools marked with the `McpTool` attribute in those classes are automatically associated with the server (for example, for [built-in](#built-in-tools) or third party tools). - By using the `servers` argument in [`McpTool` attribute](https://doc.ibexa.co/en/saas/ai/mcp/mcp_usage/#tools) to explicitly associate a specific tool with MCP servers. #### Built-in tools MCP Servers LTS Update comes with the following **experimental** built-in tools: - `Ibexa\Mcp\Tool\ContentType\ContentTypeTools` - `get_content_type` - gets a content type by its ID. - `get_content_type_by_identifier` - gets a content type by its identifier. - `get_content_type_list` - gets content types by their IDs. - `create_content_type` - creates a draft for a new content type. - `create_content_type_draft` - creates a draft for an existing content type. - `get_content_type_draft` - gets a content type draft by content type ID. - `publish_content_type_draft` - publishes a content type draft by content type ID. - `Ibexa\Mcp\Tool\ContentType\FieldDefinitionTools` - `add_field_definition` - adds a field definition to a content type draft. - `update_field_definition` - updates a field definition in a content type draft. - `remove_field_definition` - removes a field definition from a content type draft. - `Ibexa\Mcp\Tool\ContentType\ContentTypeGroupTools` - `get_content_type_groups` - gets all content type groups. - `Ibexa\Mcp\Tool\TranslationTools` - `list_languages` - lists all languages in the current SiteAccess. - `list_content_languages` - lists languages which have translations for a given content item. - `list_non_translated_content_ids` - lists IDs of content which have missing translations for a given language code. - `Ibexa\Mcp\Tool\SeoTools` - `get_non_seo_content_ids` - returns IDs of content items that are missing SEO optimization (no meta title tag). Useful for identifying content that needs SEO attention. ```yaml mcp: : path: enabled: true tools: - Ibexa\Mcp\Tool\TranslationTools - Ibexa\Mcp\Tool\SeoTools # … ``` > **Caution: Experimental tools** > > The built-in tools are experimental and may change in future releases. They are provided as examples of how to implement tools and how to configure them in an MCP server. As-is, they may not cover all your needs or may not be practical to all AI agents. If you use them, be prepared to update your MCP server configuration and tool usage when upgrading to a new version of Cohesivo. > > See how to build your own tools in [Work with MCP servers](https://doc.ibexa.co/en/saas/ai/mcp/mcp_usage/index.md). ### Discovery cache Discovery is cached to avoid scanning for capabilities on every request. You must provide a PSR-6 or PSR-16 cache pool for this caching. For example, you could set up a dedicated Redis/Valkey: ```yaml discovery_cache: cache.redis.mcp ``` For a production cluster, it's recommended to use a Redis/Valkey cache pool so the cache can be shared by all nodes. Clear the cache pool after making changes: ```bash php bin/console cache:pool:clear cache.redis.mcp ``` > **Tip: Tip** > > Use `ibexa.cache_pool` as service identifier to have the default [cache service](https://doc.ibexa.co/en/saas/infrastructure_and_maintenance/cache/persistence_cache/#cache-service). It can be set to `null` to disable caching to ease development, which isn't recommended for production environment. See another example of configuration in [Work with MCP servers](https://doc.ibexa.co/en/saas/ai/mcp/mcp_usage/#configure-mcp-server). ### Session storage MCP servers store session data in their own way. #### Options | Option | Type | Default | Description | | ----------- | ------- | ------------------ | -------------------------------------------------------------- | | `type` | enum | `psr16` | Session store type: [`psr16`](#psr-16) or [`file`](#file) | | `service` | string | `ibexa.cache_pool` | PSR-16 or PSR-6 cache service ID for the `psr16` session store | | `prefix` | string | `mcp_` | Key prefix for the `psr16` session store | | `directory` | string | `null` | Directory path for the `file` session store | | `ttl` | integer | `3600` | Session TTL in seconds | In production, it’s recommended to use [`psr16`](#psr-16) with Redis/Valkey, like with [regular sessions](https://doc.ibexa.co/en/saas/infrastructure_and_maintenance/clustering/clustering/#shared-sessions). #### PSR-16 Sessions are stored with a PSR-16 or PSR-6 compatible cache implementation. It requires that a `service` option points to a valid cache service ID. Optionally, you could use a more specific `prefix` option than the default `mcp_` to avoid key collisions with other cache usages. Such setup is suitable for production environments. ```yaml session: type: psr16 service: cache.redis.mcp prefix: 'mcp__' services: cache.redis.mcp: public: true class: Symfony\Component\Cache\Adapter\RedisTagAwareAdapter parent: cache.adapter.redis tags: - name: cache.pool clearer: cache.app_clearer provider: 'redis://mcp.redis:6379' namespace: 'mcp' ``` #### File Sessions are stored on the filesystem. This requires that you configure a directory. Such setup is suitable for development environments. In this example, sessions are stored in the `var/cache//mcp/sessions/` directory (for example, `var/cache/dev/mcp/session/` for the `dev` environment, and `var/cache/prod/mcp/sessions/` for the `prod` environment): ```yaml session: type: file directory: '%kernel.cache_dir%/mcp/sessions' ``` ### Allowed hosts This parameter lists the domains, the `Host` headers, accepted by the MCP server. The port is not part of the matching. There is no wildcard character, all cases must be listed. As item, you can use a hostname, an IP, or an IPv6. IPv6 addresses must be bracketed, for example `[::1]`. In this example, only requests from `www.example.com` domain, or from 127.0.0.1 IP are accepted: ```yaml allowed_hosts: - 'www.example.com' - '127.0.0.1' ``` # Work with MCP servers > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). Create custom capabilities for your MCP servers and test them. Editions: LTS Update The MCP Servers [LTS Update](https://doc.ibexa.co/en/saas/ibexa_products/editions/#lts-updates) includes several [built-in tools](https://doc.ibexa.co/en/saas/ai/mcp/mcp_config/#built-in-tools). Additionally, you can create your own capabilities (tools, prompts, and resources) to expose custom features to AI agents through your MCP servers. ## MCP server capabilities The Cohesivo MCP server framework (`ibexa/mcp`) is built on top of the [official PHP SDK for MCP (`mcp/sdk`)](https://github.com/modelcontextprotocol/php-sdk). A PHP class that implements MCP server capabilities such as tools, prompts, or resources must: - implement [`Ibexa\Contracts\Mcp\McpCapabilityInterface`](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-Mcp-McpCapabilityInterface.html) so that it can be scanned for capabilities - use attributes from the [`Ibexa\Contracts\Mcp\Attribute` namespace](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/namespaces/ibexa-contracts-mcp-attribute.html) to declare capabilities ### Tools The [`Ibexa\Contracts\Mcp\Attribute\McpTool` attribute](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-Mcp-Attribute-McpTool.html) declares a method as an MCP tool. It accepts the following optional arguments: - `servers` - array of server identifiers the tool is assigned to For more information, see [tools configuration](https://doc.ibexa.co/en/saas/ai/mcp/mcp_config/#tool-configuration). - `name` - tool codename - if not set, the function name is used - `title` - tool title for user interfaces - if not set, the `name` is used - `description` - tool description, used by AI agents to understand the tool's purpose - `icons` - array of [`Mcp\Schema\Icon`](https://github.com/modelcontextprotocol/php-sdk/blob/main/src/Schema/Icon.php) instances For more information, see the [`icons` specification](https://modelcontextprotocol.io/specification/2025-11-25/basic/index#icons). - `outputSchema` - associative array describing a JSON object response - `annotations` - [`Mcp\Schema\ToolAnnotations`](https://github.com/modelcontextprotocol/php-sdk/blob/main/src/Schema/ToolAnnotations.php) instance For more information, see the [`ToolAnnotations` specification](https://modelcontextprotocol.io/specification/2025-11-25/schema#toolannotations). - `meta` - free-form array for additional metadata For more information, see the [`_meta` specification](https://modelcontextprotocol.io/specification/2025-11-25/basic/index#_meta). The framework automatically builds an `inputSchema` from the method arguments and their types. To customize or extend the generated schema, you can: - add descriptions with DocBlock `@param` tags - use the [`Schema` attribute](https://github.com/php-mcp/server#-schema-generation-and-validation) If an argument is an [enum](https://www.php.net/manual/en/language.types.enumerations.php), its possible values are listed in the schema ([`UntitledSingleSelectEnumSchema`](https://modelcontextprotocol.io/specification/2025-11-25/schema#untitledsingleselectenumschema)). ### Prompts MCP servers can also provide [prompt templates](https://modelcontextprotocol.io/specification/2025-11-25/server/prompts) to help users interact with AI agents connected to the server. Methods that return a prompt are marked with the [`Ibexa\Contracts\Mcp\Attribute\McpPrompt` attribute](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-Mcp-Attribute-McpTool.html). It accepts several arguments that describe how the prompt is used: - `servers` - array of server identifiers exposing this prompt - required for prompts - `name` (optional) - prompt codename - if not set, the method name is used - `title` (optional) - prompt title - if not set, `name` is used - `description` (optional) - human-readable prompt description - `icons` (optional) - array of [`Mcp\Schema\Icon`](https://github.com/modelcontextprotocol/php-sdk/blob/main/src/Schema/Icon.php) instances For more information, see the [`icons` specification](https://modelcontextprotocol.io/specification/2025-11-25/basic/index#icons). - `meta` (optional) - rarely used free-form array for additional metadata For more information, see the [`_meta` specification](https://modelcontextprotocol.io/specification/2025-11-25/basic/index#_meta). The framework automatically builds the `arguments` array from the method arguments and their types. Prompt method arguments must be strings to comply with the [`GetPromptRequestParams` schema](https://modelcontextprotocol.io/specification/2025-11-25/schema#getpromptrequestparams). To add argument descriptions, use DocBlock `@param` tags, which are mapped to the `description` defined by the [`PromptArgument` schema](https://modelcontextprotocol.io/specification/2025-11-25/schema#promptargument). ## Example To keep the example focused on MCP server configuration and capability creation, it doesn't interact with the Cohesivo repository. ### Create user account In this example, the MCP server uses JWT tokens created with a dedicated user account. In Cohesivo's back office, create a user in the **Guest accounts** user group, with login `ibexa-example` and password `Ibexa-3xample`. ### Configure MCP server This example introduces an MCP server named `example`, with a single tool called `greet`. The server: - is enabled on the default repository - is available in all SiteAccesses - is accessible with the path `/mcp/example` For example: - `http://localhost/mcp/example` - `http://localhost/admin/mcp/example` - uses file storage for both discovery cache and sessions > **Note: Storage choice recommendations** > > Filesystem storage is convenient for the sake of this example and for testing. For production, it's recommended that you use Redis or Valkey to share cache among the cluster and improve performance. > > For development, you can set `discovery_cache: ~` to avoid clearing the cache after each change. This example uses the filesystem storage to illustrate that you have to clear the cache pool to refresh the available capabilities, exactly as when deploying into production. In a new `config/packages/mcp.yaml` file, define a new MCP server for the `default` repository and assign it to all SiteAccesses: ```yaml ibexa: repositories: default: mcp: example: path: /mcp/example enabled: true description: 'Example MCP Server' instructions: 'Use this server to greet someone.' discovery_cache: cache.tagaware.filesystem session: type: psr16 service: cache.tagaware.filesystem allowed_hosts: - '127.0.0.1' system: default: mcp: servers: - example ``` Adapt the `allowed_hosts` to your case, for example, if you want to use a domain name instead of the equivalent `127.0.0.1` address. The server is automatically registered as a service with the ID `ibexa.mcp.server.default.example`: ```bash php bin/console debug:container ibexa.mcp.server.default.example ``` An `ibexa.mcp.example` route is now available: ```bash php bin/console debug:router ibexa.mcp.example ``` ### Create capability class Create an `ExampleCapabilities` class that implements `McpCapabilityInterface`. The class contains: - a method marked with an [`McpTool` attribute](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-Mcp-Attribute-McpTool.html) that associates it with the `example` server as the `greet` tool - a method marked with an [`McpPrompt` attribute](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-Mcp-Attribute-McpPrompt.html) that provides a prompt template to users ```php */ #[McpTool( servers: ['example'], name: 'greet', title: 'User greeting', description: 'Greet a user by name', annotations: new ToolAnnotations( readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false, ), icons: [new Icon( src: 'https://openmoji.org/data/color/svg/1F44B.svg', )], outputSchema: [ 'type' => 'object', 'properties' => [ 'general' => [ 'type' => 'string', 'description' => 'the safe way to greet someone', ], 'close' => [ 'type' => 'string', 'description' => 'when you\'re close to the person, like friends or relatives', ], 'morning' => [ 'type' => 'string', 'description' => 'when it\'s in the morning', ], 'afternoon' => [ 'type' => 'string', 'description' => 'when it\'s the afternoon', ], 'evening' => [ 'type' => 'string', 'description' => 'when it\'s late in the day', ], ], ], )] public function greetByName(string $name): array { return [ 'general' => sprintf('Hello, %s!', $name), 'close' => sprintf('Hey, %s!', $name), 'morning' => sprintf('Good morning, %s!', $name), 'afternoon' => sprintf('Good afternoon, %s!', $name), 'evening' => sprintf('Good evening, %s!', $name), ]; } /** * @param string $name The name you want to be greeted by * * @return array */ #[McpPrompt( servers: ['example'], name: 'greet', title: 'Be greeted', description: 'Prompt to invoke the `greet` tool', icons: [new Icon( src: 'https://openmoji.org/data/color/svg/1F91D.svg', )], )] public function getGreetPrompt(string $name): array { return [ 'role' => 'user', 'content' => [ 'type' => 'text', 'text' => "Hi. My name is $name. Please, greet me.", ], ]; } } ``` In this example, the `servers` attribute parameter associates only this tool with the `example` server. Alternatively, you can assign all tools from the class to a server by using the `tools` parameter in the server configuration. For more information, see [tools configuration](https://doc.ibexa.co/en/saas/ai/mcp/mcp_config/#tool-configuration). For the prompt, the `servers` parameter is required. Therefore, the example prompt must use it to be associated with the `example` server. During development and testing, you may need to clear the cache to ensure that new or modified capabilities are properly re-discovered. In this example, use the following command: ```bash php bin/console cache:pool:clear cache.tagaware.filesystem ``` > **Tip: Cache clearing** > > During development, clear caches aggressively. The following commands clear all cache types, regardless of where they are stored: > > ```bash > php bin/console cache:clear > php bin/console cache:pool:clear --all > ``` ### Create MCP server list command To check the MCP server configuration, create a small command that uses the MCP server configuration registry injected through [`McpServerConfigurationRegistryInterface`](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-Mcp-McpServerConfigurationRegistryInterface.html) and autowiring: ```php configRegistry->getServerConfigurations() as $serverConfiguration) { $io->title($serverConfiguration->identifier); dump($serverConfiguration); } return Command::SUCCESS; } } ``` ### Perform `curl` test To test the `example` MCP server, a sequence of `curl` commands is used to simulate the communication between an AI client and the MCP server. - Ask for a [JWT token through REST](https://doc.ibexa.co/en/saas/api/rest_api/rest_api_reference/rest_api_reference.html#tag/User-Token/operation/api_usertokenjwt_post). - Initialize a connection to the MCP server. - Validate the MCP Session ID. - List the available tools. - Call a tool. `jq`, `grep`, and `sed` are also used to parse or display outputs. First, use the shell script to set the Cohesivo's base URL, user credentials, and MCP server URL as variables for easier reuse: ```bash baseUrl='http://localhost' # Adapt to your test case username='ibexa-example' password='Ibexa-3xample' mcpServer="$baseUrl/mcp/example" ``` Before you can communicate with the MCP server, you must first request a JWT token through the REST API: ```bash curl -s -X 'POST' \ "$baseUrl/api/ibexa/v2/user/token/jwt" \ -H 'Content-Type: application/vnd.ibexa.api.JWTInput+json' \ -H 'Accept: application/vnd.ibexa.api.JWT+json' \ -d "{ \"JWTInput\": { \"_media-type\": \"application/vnd.ibexa.api.JWTInput+json\", \"username\": \"$username\", \"password\": \"$password\" } }" > response.tmp.txt cat response.tmp.txt | jq jwtToken=$(cat response.tmp.txt | jq -r .JWT.token) rm response.tmp.txt ``` ```json { "JWT": { "_media-type": "application/vnd.ibexa.api.JWT+json", "_token": "1234567890ABCDEFGHIJKLMNOPQRSTUVWXYZ.abcdefghijklmnopqrstuvwxyz1234567890ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890abcdefghijklmnopqrstuvwxyz1234567890ABCD.EFGHIJKL-MNOPQRSTUVWXYZ12345678901234567890", "token": "1234567890ABCDEFGHIJKLMNOPQRSTUVWXYZ.abcdefghijklmnopqrstuvwxyz1234567890ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890abcdefghijklmnopqrstuvwxyz1234567890ABCD.EFGHIJKL-MNOPQRSTUVWXYZ12345678901234567890" } } ``` Then, perform [initialization](https://modelcontextprotocol.io/specification/2025-11-25/basic/lifecycle#initialization) to get an MCP session ID: ```bash cat response.tmp.txt | jq jwtToken=$(cat response.tmp.txt | jq -r .JWT.token) rm response.tmp.txt curl -s -i -X 'POST' "$mcpServer" \ -H "Authorization: Bearer $jwtToken" \ -d '{ "jsonrpc": "2.0", "id": 1, "method": "initialize", "params": { "protocolVersion": "2025-03-26", "capabilities": {}, "clientInfo": { "name": "test-curl-client", "version": "1.0.0" } } }' > response.tmp.txt sed '$d' response.tmp.txt tail -n 1 response.tmp.txt | jq mcpSessionId=$(cat response.tmp.txt | grep -i 'Mcp-Session-Id:' | sed 's/Mcp-Session-Id: \([0-9a-f-]*\).*/\1/i') rm response.tmp.txt ``` ```http HTTP/1.1 200 OK Access-Control-Allow-Headers: Content-Type, Mcp-Session-Id, Mcp-Protocol-Version, Last-Event-ID, Authorization, Accept Access-Control-Allow-Methods: GET, POST, DELETE, OPTIONS Access-Control-Allow-Origin: * Access-Control-Expose-Headers: Mcp-Session-Id Cache-Control: no-cache, private Content-Type: application/json Date: Tue, 28 Apr 2026 09:53:27 GMT Mcp-Session-Id: 12345678-9abc-def0-1234-56789abcdef0 ``` ```json { "jsonrpc": "2.0", "id": 1, "result": { "protocolVersion": "2025-06-18", "capabilities": { "logging": {}, "completions": {}, "prompts": { "listChanged": true }, "resources": { "listChanged": true }, "tools": { "listChanged": true } }, "serverInfo": { "name": "example", "version": "1.0.0", "description": "Example MCP Server" }, "instructions": "Use this server to greet someone." } } ``` Validate the initialization: ```bash curl -s -i -X 'POST' "$mcpServer" \ -H "Authorization: Bearer $jwtToken" \ -H "Mcp-Session-Id: $mcpSessionId" \ -d '{ "jsonrpc": "2.0", "method": "notifications/initialized" }' ``` ```http HTTP/1.1 202 Accepted Access-Control-Allow-Headers: Content-Type, Mcp-Session-Id, Mcp-Protocol-Version, Last-Event-ID, Authorization, Accept Access-Control-Allow-Methods: GET, POST, DELETE, OPTIONS Access-Control-Allow-Origin: * Access-Control-Expose-Headers: Mcp-Session-Id ``` Get the [list of tools](https://modelcontextprotocol.io/specification/2025-11-25/server/tools#listing-tools): ```bash curl -s -X 'POST' "$mcpServer" \ -H "Authorization: Bearer $jwtToken" \ -H "Mcp-Session-Id: $mcpSessionId" \ -d '{ "jsonrpc": "2.0", "id": 2, "method": "tools/list" }' | jq ``` ```json { "jsonrpc": "2.0", "id": 2, "result": { "tools": [ { "name": "greet", "inputSchema": { "type": "object", "properties": { "name": { "type": "string", "description": "The name of the person to greet" } }, "required": [ "name" ] }, "description": "Greet a user by name", "annotations": { "readOnlyHint": true, "destructiveHint": false, "idempotentHint": true, "openWorldHint": false }, "icons": [ { "src": "https://openmoji.org/data/color/svg/1F44B.svg" } ], "outputSchema": { "type": "object", "properties": { "general": { "type": "string", "description": "the safe way to greet someone" }, "close": { "type": "string", "description": "when you're close to the person, like friends or relatives" }, "morning": { "type": "string", "description": "when it's in the morning" }, "afternoon": { "type": "string", "description": "when it's the afternoon" }, "evening": { "type": "string", "description": "when it's late in the day" } } } } ] } } ``` [Call](https://modelcontextprotocol.io/specification/2025-11-25/server/tools#calling-tools) the `greet` tool: ```bash curl -s -X 'POST' "$mcpServer" \ -H "Authorization: Bearer $jwtToken" \ -H "Mcp-Session-Id: $mcpSessionId" \ -d '{ "jsonrpc": "2.0", "id": 3, "method": "tools/call", "params": { "name": "greet", "arguments": { "name": "World" } } }' | jq ``` ```json { "jsonrpc": "2.0", "id": 3, "result": { "content": [ { "type": "text", "text": "{\n \"general\": \"Hello, World!\",\n \"close\": \"Hey, World!\",\n \"morning\": \"Good morning, World!\",\n \"afternoon\": \"Good afternoon, World!\",\n \"evening\": \"Good evening, World!\"\n}" } ], "isError": false, "structuredContent": { "general": "Hello, World!", "close": "Hey, World!", "morning": "Good morning, World!", "afternoon": "Good afternoon, World!", "evening": "Good evening, World!" } } } ``` Get the [list of prompts](https://modelcontextprotocol.io/specification/2025-11-25/server/prompts#listing-prompts): ```bash curl -s -X 'POST' "$mcpServer" \ -H "Authorization: Bearer $jwtToken" \ -H "Mcp-Session-Id: $mcpSessionId" \ -d '{ "jsonrpc": "2.0", "id": 4, "method": "prompts/list" }' | jq ``` ```json { "jsonrpc": "2.0", "id": 4, "result": { "prompts": [ { "name": "greet", "description": "Prompt to be greeted by the `greet` tool", "arguments": [ { "name": "name", "description": "The name you want to be greeted by", "required": true } ], "icons": [ { "src": "https://openmoji.org/data/color/svg/1F91D.svg" } ] } ] } } ``` [Get the prompt](https://modelcontextprotocol.io/specification/2025-11-25/server/prompts#getting-a-prompt) of the `greet` method: ```bash curl -s -X 'POST' "$mcpServer" \ -H "Authorization: Bearer $jwtToken" \ -H "Mcp-Session-Id: $mcpSessionId" \ -d '{ "jsonrpc": "2.0", "id": 5, "method": "prompts/get", "params": { "name": "greet", "arguments": { "name": "Firstname Lastname" } } }' | jq ``` ```json { "jsonrpc": "2.0", "id": 5, "result": { "messages": [ { "role": "user", "content": { "type": "text", "text": "Hi. My name is Firstname Lastname. Please, greet me." } } ] } } ``` ### Perform MCP Inspector test You can test your server with the [MCP Inspector](https://modelcontextprotocol.io/docs/tools/inspector). You still need to ask for a JWT token through REST or GraphQL APIs, and use it in the MCP Inspector configuration to connect to the server. You can use a web interface to obtain the JWT token: - [REST live documentation](https://doc.ibexa.co/en/saas/api/rest_api/rest_api_authentication/#jwt-token-obtained-through-rest-documentation) - [GraphiQL](https://doc.ibexa.co/en/saas/api/graphql/graphql/#jwt-authentication) #### MCP server settings In this example, the settings needed to use the MCP Inspector are as follows: - Transport Type: Streamable HTTP - URL: actual domain and server `path`, for example `http://localhost/mcp/example` - Connection Type: Via Proxy - Authentication: - Custom Headers: - ☑ `Authorization` - `Bearer ` - OAuth 2.0 Flow: left unedited ![Left panel of MCP Inspector with connection settings for MCP server](https://doc.ibexa.co/en/saas/ai/mcp/img/mcp-inspector-config.png "MCP Inspector connection settings") #### Test MCP server within MCP Inspector In the right panel, in the **Tools** tab, click **List Tools** in the left column. The `greet` tool appears, preceded by its icon. You can select and test it in the right column. ![Right panel of MCP Inspector with a list of tools obtained from MCP server, and the test of the greet tool](https://doc.ibexa.co/en/saas/ai/mcp/img/mcp-inspector-greet-tool.png "MCP Inspector greet tool test") In the **Prompts** tab, in the left column, click **List Prompts**. The `greet` prompt appears, preceded by its icon. You can select and test it in the right column. ![Right panel of MCP Inspector with a list of prompts obtained from the MCP server, and the test of the greet prompt](https://doc.ibexa.co/en/saas/ai/mcp/img/mcp-inspector-greet-prompt.png "MCP Inspector greet prompt test") ### Perform Copilot or Claude Code test You can test your MCP server with [Copilot CLI](https://docs.github.com/en/copilot/concepts/agents/copilot-cli/about-copilot-cli) or [Claude Code CLI](https://code.claude.com/docs/en/overview), as illustrated here, or with any other agent or interface. #### Add MCP server to agent CLI For the sake of the agent test, in this example, you configure the MCP server in an `.mcp.json` file at the Cohesivo project root. This way, it is only available for a session opened from there. You can handle the JWT token for this test in the following ways: - [Hard-code the JWT token](#hard-coded-variant) into the configuration and update it at every expiration. - [Wrap a JWT token request and an MCP server call into a script](#fully-scripted-variant). ##### Hard-coded variant The hard-coded JWT token configuration in `.mcp.json` looks as follows: ```json { "mcpServers": { "ibexa-example": { "type": "http", "url": "http://localhost/mcp/example", "headers": { "Authorization": "Bearer " }, "tools": ["*"] } } } ``` The `.mcp.json` file must be edited to update the JWT token each time it expires. You can request a token by using the GraphiQL web interface or a `curl` command, and then edit the file manually. Alternatively, you can configure a shell script to request the JWT token, extract it from the response, and replace it in the file. When Copilot or Claude Code complains that it can't communicate with the MCP server: **Copilot CLI** - Update the JWT token in the `.mcp.json` file. - Reload the MCP servers in Copilot CLI with one of these methods: - Run `/mcp reload` command to reload all MCP servers. - Run `/mcp disable ibexa-example` and `/mcp enable ibexa-example` to only reload the `ibexa-example` server. > **Note: Reloading multiple MCP servers** > > If you have several MCP servers enabled globally, reloading all of them at the same time can be time-consuming. Consider reloading them one by one. **Claude Code CLI** - Update the JWT token in the `.mcp.json` file. - Run `/mcp reconnect ibexa-example` command to reconnect the `ibexa-example` MCP server. ##### Fully scripted variant The wrapping script configuration in `.mcp.json` looks as follows: ```json { "mcpServers": { "ibexa-example": { "type": "stdio", "command": "bash", "args": ["mcp-ibexa-example-wrapper.sh"], "tools": ["*"] } } } ``` `mcp-ibexa-example-wrapper.sh` is a script that requests a JWT token and establishes a connection with the MCP server. For example, thanks to [`npx`](https://www.npmjs.com/package/npx), you can do it with [Supergateway](https://www.npmjs.com/package/supergateway) without a local installation: ```bash #!/bin/bash set -e baseUrl='http://localhost' # Adapt to your test case mcpServer="$baseUrl/mcp/example" jwtToken=$(curl -s -X 'POST' \ "$baseUrl/api/ibexa/v2/user/token/jwt" \ -H 'Content-Type: application/vnd.ibexa.api.JWTInput+json' \ -H 'Accept: application/vnd.ibexa.api.JWT+json' \ -d '{ "JWTInput": { "_media-type": "application/vnd.ibexa.api.JWTInput+json", "username": "ibexa-example", "password": "Ibexa-3xample" } }' | jq -r .JWT.token) exec npx -y supergateway \ --streamableHttp "$mcpServer" \ --oauth2Bearer "$jwtToken" \ --logLevel none ``` When the agent complains that it can't communicate with the MCP server, reload it: **Copilot CLI** Reload the MCP servers in Copilot CLI with one of these methods: - Run `/mcp reload` command to reload all MCP servers. - Run `/mcp disable ibexa-example` and `/mcp enable ibexa-example` to only reload the `ibexa-example` server. > **Note: Reloading multiple MCP servers** > > If you have several MCP servers enabled globally, reloading all of them at the same time can be time-consuming. Consider reloading them one by one. **Claude Code CLI** Run `/mcp reconnect ibexa-example` command to reconnect the `ibexa-example` MCP server. #### Run MCP server test with Copilot CLI or Claude Code CLI Launch the agent CLI at the project root, where the `.mcp.json` file is located: **Copilot CLI** ```bash cd /path/to/project copilot ``` **Claude Code CLI** ```bash cd /path/to/project claude ``` If prompted, confirm that you trust the files in this folder. You may choose to have your choice remembered for the future. You can check the MCP server status and details with the `/mcp` command: **Copilot CLI** Run the `/mcp show ibexa-example` command to check the MCP server status and details: ```text MCP Server: ibexa-example Type: stdio Command: bash Status: ✓ Connected Source: /path/to/project/.mcp.json Tools (1/1 enabled): ✓ greet: Greet a user by name (70 tokens) ``` **Claude Code CLI** Run the `/mcp` command and select ibexa-example to check the MCP server status and details: ```text Ibexa-example MCP Server Status: ✔ connected Command: bash Args: mcp-ibexa-example-wrapper.sh Config location: /path/to/project/.mcp.json Capabilities: tools Tools: 1 tool ``` You can prompt the agent to greet you. It should use the `greet` tool to do so. - You can ask it "Please, greet me." and it might respond with a request for your name if it doesn't already know it. - You can also give it another name to greet, as shown in the prompt template. During a morning session, the interaction could look like this: **Copilot CLI** ```text ❯ Hi. Please, greet me. ◐ The user wants to be greeted. I'll use the ibexa-example-greet tool, but I don't know their name. Let me ask them first... actually, the tool requires a name. Let me ask the user for their name. Wait, actually I can just greet them without knowing their name using a general greeting, but the tool requires a name parameter. Let me ask for their name. ● Asked user What's your name? └ Pulley ● Greet a user (MCP: ibexa-example) · name: "Pulley" └ {"general":"Hello, Pulley!","close":"Hey, Pulley!","morning":"Good morning, P... ● Good morning, Pulley! ☀️ ❯ Hi. Please, greet me. We're close friends. ◐ The user wants a close friend greeting. From the previous response, I can see the "close" greeting is "Hey, Pulley!" ● Greet a user (MCP: ibexa-example) · name: "Pulley" └ {"general":"Hello, Pulley!","close":"Hey, Pulley!","morning":"Good morning, P... ● Hey, Pulley! 😄 ❯ Please, greet someone named "ElePHPant" in a general way (so I could reuse it at any time) ◐ The user wants a general greeting for someone named "ElePHPant". ● greet (MCP: ibexa-example) · name: "ElePHPant" └ {"general":"Hello, ElePHPant!","close":"Hey, ElePHPant!","morning":"Good morn... ● Hello, ElePHPant! 🐘 ❯ Please, greet someone named "ElePHPant" like it's 22:30 ◐ 22:30 is evening time, so I'll use the evening greeting. ● greet (MCP: ibexa-example) · name: "ElePHPant" └ {"general":"Hello, ElePHPant!","close":"Hey, ElePHPant!","morning":"Good morn... ● Good evening, ElePHPant! 🌙 ``` **Claude Code CLI** ```text ❯ Hi. Please, greet me. ⏺ What's your name? ✻ Worked for 3s ❯ Pulley Called ibexa-example ⏺ Hello, Pulley! 👋 ✻ Churned for 4s ❯ Hi. Please, greet me. We're close friends now. Called ibexa-example ⏺ Hey, Pulley! 👋 ✻ Baked for 4s ❯ Please, greet someone named "ElePHPant" in a general way (so I could reuse it at any time) Called ibexa-example ⏺ Hello, ElePHPant! ✻ Brewed for 5s ❯ Please, greet someone named "ElePHPant" like it's 22:30 ⏺ That falls under the "evening" variant: Good evening, ElePHPant! ✻ Sautéed for 2s ``` The agent's reflections, reaction times, and final responses, including the improvised emojis, may differ from those examples. The key point is that the agent decides to use the `greet` tool, calls it with the right argument, and then uses the call result in its final output. You can fine-tune the prompt, or remove unnecessary variants if needed. For example, you could instruct the agent to always use the time-of-day variants, or simply remove the `general` and `close` variants. Removing what's unnecessary is more efficient than extending the instructions. # Product catalog # Product Catalog > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). Cohesivo provides product catalog capabilities for managing products, product types, variants, attributes, pricing, and catalogs. The Product Catalog provides comprehensive capabilities for managing products offered in your digital commerce experience, including their specifications, pricing, and organization. Cohesivo offers robust product catalog infrastructure that can be used standalone. You can also use [Quable](https://doc.ibexa.co/en/saas/product_catalog/quable/quable/index.md) add-on that fully integrates into the Ibexa ecosystem, or the [Remote PIM](https://doc.ibexa.co/en/saas/product_catalog/add_remote_pim_support/index.md) to add integration with any external PIM system. - [Product catalog guide](https://ez-systems-developer-documentation--3381.com.readthedocs.build/en/3381/product_catalog/product_catalog_guide/): The product catalog guide provides a full description of the features and capabilities for managing products, their specifications, variants, pricing, and organization. - [Quable Integration](https://ez-systems-developer-documentation--3381.com.readthedocs.build/en/3381/product_catalog/quable/quable/): Quable integration with Cohesivo - [Products](https://ez-systems-developer-documentation--3381.com.readthedocs.build/en/3381/product_catalog/products/): Products are characterized by attributes describing their characteristics. You can create product variants and add assets to each product and variant. - [Catalogs](https://ez-systems-developer-documentation--3381.com.readthedocs.build/en/3381/product_catalog/catalogs/): Catalogs enable filtering out a selection of products from the Product catalog. - [Product catalog configuration](https://ez-systems-developer-documentation--3381.com.readthedocs.build/en/3381/product_catalog/product_catalog_configuration/): Configure product catalog settings per repository, with different catalog engines and VAT configurations. - [Prices](https://ez-systems-developer-documentation--3381.com.readthedocs.build/en/3381/product_catalog/prices/): The price engine calculates product prices taking into account customer groups, currencies and taxes. # Product catalog guide > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). The product catalog guide provides a full description of the features and capabilities for managing products, their specifications, variants, pricing, and organization. ## What is product catalog The product catalog is a comprehensive set of capabilities for managing products in Cohesivo that can be used standalone. It lets you create, configure, and manage products, their specifications, assets, variants, and prices, and group products into categories and catalogs. ## Availability Product catalog capabilities are available in all Cohesivo editions. ## How does product catalog work Products in Cohesivo’s product catalog have underlying content items enriched with product-specific information such as attributes, assets, prices, and others. The product catalog lets you group products into categories and catalogs. Catalogs are collections of products selected by using configurable filters. They're specific to each of your sites or storefronts and only contain the products in them that you wish to sell in their associated storefronts. Catalogs contain a complete list of related products that can be displayed on a store site. You can have as many catalogs as required. ![How does product catalog work](https://doc.ibexa.co/en/saas/product_catalog/img/how_pim_works.png) ## Capabilities ### Product specifications Product specifications rely on product attributes. Available attributes are defined per product type. ### Product attributes Each product has its own, specific attributes. You can describe a product in technical terms, define its physical characteristics such as size, color, or shape, or functional characteristics (for example, for a laptop it could be the operating system, amount of memory, or available ports). Product attributes can belong to one of existing types (for example, numbers, selection, or checkout), but you can also [add custom attribute types](https://doc.ibexa.co/en/saas/product_catalog/create_custom_attribute_type/index.md). Attributes are used as criteria for filtering and searching for products. You can also configure selected product attributes to be used as a basis for variants. ![Product attributes](https://doc.ibexa.co/en/saas/product_catalog/img/product_attributes.png) For more information, see [Product attributes](https://doc.ibexa.co/en/saas/product_catalog/products/#product-attributes) and [Work with product attributes](https://doc.ibexa.co/projects/userguide/en/6.0/product_catalog/work_with_product_attributes/) ### Product variants One product can have multiple versions, for example, there can be a t-shirt in different colors. You can [create variants of products](https://doc.ibexa.co/en/saas/product_catalog/product_api/#creating-variants), differing in some characteristics, based on product attributes. ![Product variants](https://doc.ibexa.co/en/saas/product_catalog/img/product_attributes.png) ### Product assets Each product or product variant can have assets in a form of images. They can be assigned to the base product or per one or more of its variants. For easier management you can create collections - by using them you can group assets that correspond to specific values of attributes. Created collection is automatically assigned to the variant or variants that have these attribute values. ![Products assets](https://doc.ibexa.co/en/saas/product_catalog/img/product_assets.png) ### Availability Product availability defines whether a product is available in the catalog. For each product you can [set availability](https://doc.ibexa.co/projects/userguide/en/6.0/product_catalog/manage_availability_and_stock/) per variant or per base product. When a product is available, it can have numerical stock defined, that you can set. The stock can also be set to infinite, for example, for digital, downloadable products. A product can only be ordered when it has either positive stock, or stock set to infinite. ### Product categories Product categories help you to organize your products within the product catalog and also create relationships between them. Each product can belong to multiple categories of, depending on user’s choice, different or similar character. Category can also be assigned to multiple products. One of the reasons for applying product categories is assisting users in searching for products. Before you can assign categories to products, you need to [enable product categories](https://doc.ibexa.co/projects/userguide/en/6.0/product_catalog/work_with_product_categories/#enable-product-categories). ![Product categories](https://doc.ibexa.co/en/saas/product_catalog/img/product_categories.png) ### Virtual and physical products Product types in Cohesivo can be either virtual or physical: - **Physical products** are tangible items that require shipping (for example: books, clothing, electronics). - **Virtual products** are items that don't require physical delivery (for example: software licenses, e-books, online courses, digital downloads, additional warranty, tickets for an event). This product type property can affect the checkout process. For example, a cart of only virtual products can skip the shipping step during checkout. To learn more about working with virtual products, see [Virtual products](https://doc.ibexa.co/projects/userguide/en/6.0/product_catalog/create_virtual_product/) in the User Documentation. ### Currencies Currencies are used when calculating product price. In the system you can find a list of available currencies, but you can also create custom ones by providing its code. ### Regions Each product or product type can have different regional pricing and regional VAT rate. You can configure regions in [YAML configuration](https://doc.ibexa.co/en/saas/product_catalog/enable_purchasing_products/#region-and-currency). ### VAT For each product you can configure VAT rate. You can set it globally (per SiteAccess) or individually for each product type and product. To set up different VAT rates for different regions (countries),you need to first configure them in [YAML configuration](https://doc.ibexa.co/en/saas/product_catalog/enable_purchasing_products/#vat-rates). ### Base price For each product or product variant you can set a base price. If you use more than one currency, in the product’s page you can see base price per currency. ### Custom price You can set up different prices depending on customer group or currency. Each customer group can have a default price discount that applies to all products. For example, you can offer a 10% discount for all products in the catalog to users who belong to the Resellers customer group. You can also set different prices for specific products or product variants for different customer groups. ### Product completeness Created product has its own list of the tasks required for product configuration: attributes, assets, content, prices, availability, and more. You can check how complete the configuration is in the product’s view. When you create or edit a product, under the product name, you can see visual indication of what part of product information (tasks) you have completed, and what part is still missing. Product completeness doesn't impact product availability or visibility on the storefront. It is intended to help you ensure that product data is properly populated. As long as your product meets [basic requirements](https://doc.ibexa.co/en/saas/product_catalog/enable_purchasing_products/index.md), it can be published and made available for purchase regardless of its completeness score. ### Catalogs With catalogs you can create product lists for special purposes, for example, for B2B and B2C uses, for retailers and distributors, or for different regions. Catalogs contain a sub-set of products from the system. You can copy existing catalogs, for example, to create a variant version of an offer with slightly differing filters. You can then modify the copied catalog and save the updated version. ### Catalog filters and custom filter When you create a new catalog, all products are included in it by default. To have a better overview for a specific group of products, you can filter the list by: - price (Solr or Elasticsearch only) - product attributes - product type - product code - availability - product category - the date when the product was created Catalog filters let you narrow down the products from the product catalog that are available in the given catalog. Besides, the built-in catalog filters, you can also [create custom ones](https://doc.ibexa.co/en/saas/product_catalog/create_custom_catalog_filter/index.md). ### Remote PIM support Cohesivo provides flexible product catalog infrastructure that works with external PIM systems. In Cohesivo, products are created and maintained by using the REST API or the back office, and their data is stored in a local database. However, in your project or organization, you might have an existing product database, or be specifically concerned about product information security. To address such needs, Cohesivo provides remote PIM support. You can install and configure a readily available [Quable integration](https://doc.ibexa.co/en/saas/product_catalog/quable/quable/index.md) add-on, or build a custom one to connect to a remote PIM or ERP system, pull product data and present it on your website. ![Remote PIM](https://doc.ibexa.co/en/saas/product_catalog/img/remote_pim_support.png) An example implementation is delivered as an optional package that you can [install and customize](https://doc.ibexa.co/en/saas/product_catalog/add_remote_pim_support/index.md) to fulfill your requirements. #### Capabilities With remote PIM support, you can take advantage of the following capabilities: ##### Product marketing Use the product information coming from another system in your marketing campaigns to promote certain products or brands. By embedding the products within content items and landing pages, you can leverage Cohesivo marketing capabilities to showcase products. ##### Pricing, stock and availability A product can only be ordered when it has defined [availability](https://doc.ibexa.co/projects/userguide/en/6.0/product_catalog/manage_availability_and_stock/), stock and [pricing information](https://doc.ibexa.co/projects/userguide/en/6.0/product_catalog/manage_prices/). By default, such information is held in the Cohesivo's local database. In your specific scenario, you can implement the support for availability and pricing information coming from an external source as well, by using a price/availability matching strategy that is an extension point exposed in the Product catalog module. #### Limitations The limitation of remote PIM depend on implementation details of specific integration and may arise in areas relying on [content model](https://doc.ibexa.co/en/saas/content_management/content_model/index.md). To see the limitations of the Quable integration add-on, see [Quable known limitations](https://doc.ibexa.co/en/saas/product_catalog/quable/quable_guide/#known-limitations). ##### Searching Filtering and pagination function the same as with the product catalog, relying on product attributes for effective organization of product data. However, criteria and sort clauses within product catalog relying on Cohesivo's content model are not supported. Depending on your source of product information, you might need to adjust the implementation to be compatible with your data format. For reference, you could review the [`CriterionVisitor` class](https://github.com/ibexa/example-in-memory-product-catalog/blob/main/src/lib/PIM/InMemory/CriterionVisitor.php) that is part of [Remote PIM example package](https://doc.ibexa.co/en/saas/product_catalog/add_remote_pim_support/#install-remote-pim-example-package). For more information about product search, see [Product Search Criteria reference](https://doc.ibexa.co/en/saas/search/criteria_reference/product_search_criteria/index.md) and [Product Sort Clauses](https://doc.ibexa.co/en/saas/search/sort_clause_reference/product_sort_clauses/index.md). ##### Catalogs Depending on the implementation, creating [catalogs](#catalogs) might be supported, but the criteria for filtering can be limited. The default implementation, which serves as a basis for the example remote PIM package, has some limitations: certain functionalities either don't operate or operate within defined constraints. Therefore, if your specific requirements aren't met, you may need to extend Cohesivo. ##### Editing product types, products and product attributes Editing product type, product and product attribute information stored in the remote PIM is impossible due to their read-only status. This means that, functionally speaking, communication with PIM is uni-directional, and information is pulled from a remote source but cannot be updated. ##### Content-model-based features The following features rely on Cohesivo's content model capabilities, which aren't supported by the default implementation of remote PIM support. Therefore, if your specific requirements aren't met, you must extend the application by using extension points exposed in the product catalog module. - Assets - Product variants - Product categories - Taxonomy - URL aliases ##### Simplified presentation of product-related blocks and views Enabling Remote PIM impacts a number of application views and blocks, such as Product view, Product list, Catalog, and Product Collection. They're simplified, for example, they don't include thumbnails and other assets, or refer to URL aliases. You can customize them by extending the default implementation. ##### Limited HTTP Caching In the context of remote PIM, it's impossible to use [content-aware HTTP caching](https://doc.ibexa.co/en/saas/infrastructure_and_maintenance/cache/http_cache/content_aware_cache/index.md) with `ibexa_http_cache_tag_relation_ids`. ## How to get started To start working with the products, you need to enable purchasing from the catalog. For this, the following configuration is required: - at least one region and one currency added in the shop, - VAT rates set for the product type, - at least one price added for the product, - availability of the product set with positive or infinitive stock. Next, follow steps from [product management in User Documentation](https://doc.ibexa.co/projects/userguide/en/6.0/persona_paths/manage_products/). ## Benefits ### Product technical and marketing information Products added in the shop have both technical and marketing information. You can see all the attributes, specification and variants in a very detailed way, which helps you to manage and present all the products in the technical way. In addition, each product and its variants may have assets in the form of images and a description. Products have underlying content items, which means you can customize the content structure to contain all the marketing information about the product that you need. ### Detailed specification with multiple attribute types Product attributes help you to create products with detailed, complicated specification. Thanks to this, you can create product variants based on multiple product attributes that include different information about a product. Additionally, product attributes are collected in groups so they're easier to manage. ![Multiple attribute types](https://doc.ibexa.co/en/saas/product_catalog/img/product_attribute_types.png) ### Multiple-level variants Product variants enable you to have multiple versions of one product, differing in some characteristics. Each product can have more than one variant on one or more levels. It makes it possible to have multiple-level variants of the products complicated in terms of specifications, such as laptops. ![Multiple-level variants](https://doc.ibexa.co/en/saas/product_catalog/img/multilevel_variants.png) ### Extensible availability By default, you can configure products with specific number in stock, or with infinite availability. You can also extend the availability mechanism to cover other use cases, such as pre-orders. ### Regional pricing including regional VAT rates Each product type can have different regional pricing and regional VAT rate. What is more, you can configure VAT rate globally or set it individually. Thanks to this, the management of the products that can be sold to various markets is easier and more intuitive. ![Regional pricing](https://doc.ibexa.co/en/saas/product_catalog/img/regional_vat.png) ### Customer group-based pricing You can set up different prices depending on customer group - it means that you can have a default price discount for different customer groups that applies to all the products or specific products or product variants. ![Customer group-based pricing](https://doc.ibexa.co/en/saas/product_catalog/img/group_base_pricing.png) ### Product taxonomy The [taxonomy mechanism](https://doc.ibexa.co/en/saas/content_management/taxonomy/taxonomy/index.md) enables creating tags or categories with a tree structure and assign them to a content item, for example, Products. Thanks to this mechanism product categories can be organized into a Category tree to make it easy for the users to browse and to deliver content appropriate for them. ### Grouping products into catalogs You can group all the products into smaller catalogs. They contain subsets of the whole product list and you can use them to build special catalogs, for example, for retailers and distributors, or for different regions. ![Grouping products into catalogs](https://doc.ibexa.co/en/saas/product_catalog/img/grouping_products.png) ### General and variant-specific assets Products and product variants can have their image assets. You can set up general assets — it means that the product has an asset visible in the main product view. Additionally, you can assign assets to product variants and place them in a collection. ![General and variant-specific assets](https://doc.ibexa.co/en/saas/product_catalog/img/general_assets.png) # Quable Integration > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). Quable integration with Cohesivo Cohesivo integrates with [Quable](https://www.quable.com/en) to provide product information management as part of the Ibexa orchestration platform. Quable is Ibexa’s PIM solution for managing complex product catalogs and serves as the single source of truth, available as an add-on for Cohesivo. Once you install and configure it, the integration performs an initial synchronization of product data, followed by ongoing updates via webhooks. Products can be viewed, selected, and embedded in Cohesivo, while all product management operations remain handled in Quable. ## Getting started - [Quable product guide](https://ez-systems-developer-documentation--3381.com.readthedocs.build/en/3381/product_catalog/quable/quable_guide/): The Quable product guide describes how you can use the product data from Quable in Cohesivo to create marketing campaigns built around your products. - [Quable PIM integration](https://doc.ibexa.co/projects/userguide/en/6.0/product_catalog/quable_pim_integration/): Quable PIM integration allows you to use products managed in Quable as the source of product data in Ibexa DXP. - [Quable - PIM solution for product data management](https://www.quable.com/en): Manage your product data and accelerate sales with Quable. Discover the new PIM platform that revolutionizes the product experience - [Quable resources](https://docs.quable.com/): Find all PIM, DAM, and Portal resources: user guides, training content, product documentation, technical documentation, and the PIM API for developers. ## Development - [Install Quable connector](https://ez-systems-developer-documentation--3381.com.readthedocs.build/en/3381/product_catalog/quable/install_quable/): Install and configure Quable connector for Cohesivo - [Configure Quable connector](https://ez-systems-developer-documentation--3381.com.readthedocs.build/en/3381/product_catalog/quable/configure_quable_connector/): Quable connector configuration reference for Cohesivo - [Quable API](https://ez-systems-developer-documentation--3381.com.readthedocs.build/en/3381/product_catalog/quable/quable_api/): Learn how to use PHP and REST APIs to retrieve product data from Quable - [Quable technical documentation](https://developers.quable.com/): Explore Quable's technical documentation # Quable product guide > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). The Quable product guide describes how you can use the product data from Quable in Cohesivo to create marketing campaigns built around your products. ## Overview Quable integration connects Cohesivo with [Quable](https://www.quable.com/en), making Quable the authoritative source of product information for every website powered by Cohesivo. Quable serves as the single source of truth for all product data, including attributes, classifications, variants, and translations. Cohesivo consumes this data and makes it available for use in content and digital experiences. This approach eliminates the need to manage product data in multiple systems, while preserving a clear separation of responsibilities between product management and content usage. ## Availability The integration with Quable is available as an add-on for all Cohesivo editions. Before installing and enabling the add-on, ensure that you have an active Quable instance with defined products, classifications, and channels. Then, [perform the initial configuration](https://doc.ibexa.co/en/saas/product_catalog/quable/install_quable/index.md). ## How does Quable integration work The integration is built on Cohesivo's [Remote PIM framework](https://doc.ibexa.co/en/saas/product_catalog/add_remote_pim_support/index.md), which enables connection to external product data sources. Once configured, the system performs: - an initial synchronization of product data from Quable - ongoing updates via webhooks (near real-time) Product data is mapped to the Cohesivo's product data model, including variants, attributes and [product categories](https://doc.ibexa.co/en/saas/product_catalog/product_catalog_guide/#product-taxonomy). This data is then available in the back office, content editing tools like [Online Editor](https://doc.ibexa.co/en/saas/content_management/rich_text/online_editor_guide/index.md) and [Page Builder](https://doc.ibexa.co/en/saas/content_management/pages/page_builder_guide/index.md), and APIs. All product management operations remain handled in Quable. Cohesivo can be used to manage pricing and availability for products sourced from Quable, including support for market-specific configurations such as regions and currencies. ## Capabilities ### Single source of truth Quable is the authoritative system for product data, including attributes, classifications, variants, and translations. Cohesivo consumes this data and makes it available for use within content and back office interfaces, enabling editorial teams to enrich content by reusing product information. ## Use cases ### Multi-market operations A retailer operating across multiple markets can manage product data in Quable using channels and localized languages. Cohesivo connects to the relevant channel and makes localized product information available for use in content and back office interfaces, ensuring consistency across markets from a single Quable instance. ## Faster campaign execution Product data defined in Quable can be immediately used in Cohesivo for building content and campaigns. Marketing teams can create pages and enrich content using up-to-date product information, without the need to duplicate or manually synchronize data. ## Known limitations The integration with Quable has the following known limitations: - [Catalogs](https://doc.ibexa.co/en/saas/product_catalog/product_catalog_guide/#catalogs) can't be created from Quable products. - [Product assets](https://doc.ibexa.co/en/saas/product_catalog/product_catalog_guide/#product-assets) are not fully synchronized. Only the main product thumbnail from Quable is used. - [Product-level access restrictions](https://doc.ibexa.co/en/saas/permissions/policies/#products) based on product type are not supported. - You can't define prices and availability for products with product codes exceeding 64 characters. # Install Quable connector > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). Install and configure Quable connector for Cohesivo To integrate Cohesivo with Quable, you need to install the Quable connector packages, configure the connection, and set up synchronization. ## Create Quable instance Before installing the Quable connector, ensure you have access to a [Quable instance](https://www.quable.com). ## Install package Run the following command to install the required package: ```bash composer require ibexa/connector-quable ``` The command adds the Quable connector code, including services that enable communication with Quable. ## Get API credentials To connect to Quable, you need an API token: 1. Log in to your Quable instance, for example, `https://example.quable.com`. 2. Navigate to the [API Tokens](https://docs.quable.com/v5-EN/docs/api-tokens) section. 3. Create a new **Read Access Token** for use in the configuration. ## Configure Quable connector In `config/packages/ibexa_connector_quable.yaml`, specify the configuration for the Quable connector: ```yaml ibexa_connector_quable: instance_url: 'https://example.quable.com' api_token: '' channel_code: '' ``` Replace `` with the Read Access API token you obtained from Quable in the previous step. [Quable's channels](https://docs.quable.com/v5-EN/docs/content-channels) allow you to distribute your product information to defined recipients, for example e-commerce platforms. Select the Quable channel that you want to integrate within Cohesivo. For all available configuration options, see [Configure Quable](https://doc.ibexa.co/en/saas/product_catalog/quable/configure_quable_connector/index.md). ## Configure product catalog engine To use Quable as a product data source, configure Cohesivo's [product catalog](https://doc.ibexa.co/en/saas/product_catalog/product_catalog_guide/index.md) to use the Quable engine. ### Define Quable engine In `config/packages/ibexa_product_catalog.yaml`, add a new engine configuration: ```yaml ibexa_product_catalog: engines: local: type: local options: root_location_remote_id: ibexa_product_catalog_root product_type_group_identifier: product quable: type: quable options: taxonomy: quable root_location_remote_id: ibexa_product_catalog_root product_type_group_identifier: product ``` This configuration defines two engines: the default `local` engine and the new `quable` engine, allowing you to work with products defined within Quable. To learn more about product catalog configuration, see [Product catalog configuration](https://doc.ibexa.co/en/saas/product_catalog/product_catalog_configuration/index.md). The Quable integration add-on comes with a new [taxonomy](https://doc.ibexa.co/en/saas/content_management/taxonomy/taxonomy/index.md) called `quable`. By setting the `ibexa_product_catalog.engines.quable.options.taxonomy` key to `quable`, you configure the engine to use it for storing product categories. ### Set Quable as default engine In your repository configuration, typically in `config/packages/ibexa.yaml`, configure the product catalog to use the Quable engine as the product data source: ```yaml ibexa: repositories: default: storage: ~ search: engine: '%search_engine%' connection: default product_catalog: engine: quable regions: default: ~ ``` ## Set up languages To use the products from Quable within Cohesivo content, make sure the [data languages](https://docs.quable.com/v5-EN/docs/data-languages) in Quable have corresponding [languages](https://doc.ibexa.co/en/saas/multisite/languages/languages/index.md) in Cohesivo. To compare the language configuration in both systems, run the following command: ```bash php bin/console ibexa:quable:languages:check ``` Based on the command output, configure the `language_map` in `config/packages/ibexa_connector_quable.yaml`, mapping each Cohesivo language code to its Quable locale code as in the following example: ```yaml ibexa_connector_quable: # ... language_map: eng-GB: en_GB fre-FR: fr_FR ``` The system uses the language map to retrieve data in the correct language from Quable. After configuring the map, rerun the `ibexa:quable:languages:check` command to confirm all languages are correctly mapped. ## Synchronize taxonomy After configuring the integration, synchronize [product classifications from Quable](https://docs.quable.com/v5-EN/docs/documents-classification-new-version) to Cohesivo's [taxonomies](https://doc.ibexa.co/en/saas/content_management/taxonomy/taxonomy/index.md). Run the following command to synchronize classifications: ```bash php bin/console ibexa:quable:classification:sync ``` This command imports the product classification structure from Quable into Cohesivo, ensuring that product categories are aligned. > **Tip: Tip** > > To keep the classifications aligned, it's recommended that you run the `ibexa:quable:classification:sync` command every night, even when using synchronization with webhooks. ## Set up real-time synchronization Quable can notify Cohesivo about product data and classification changes in real-time by using webhooks. This invalidates the cache kept in Cohesivo, ensuring that product information stays up to date. Webhook configuration must be set up in both Quable and Cohesivo. ### Create webhook in Quable 1. Create a new [webhook in Quable](https://docs.quable.com/v5-EN/docs/webhook). 2. Set the webhook code (used as the webhook name). 3. Provide the URL to your Cohesivo instance suffixed by `/webhook/quable`, for example: `https://example.com/webhook/quable`. 4. Mark it as **Activated**. 5. Enter a secret value for the **Authorization Header**. 6. Choose the following scopes: - Products: created, updated, deleted - Classifications: created, updated, deleted The **Authorization Header** value is a [secret that must be kept secure](https://doc.ibexa.co/en/saas/infrastructure_and_maintenance/security/security_checklist/#app_secret-and-other-secrets). > **Note: Note** > > For local development and testing, you can consider using one of the available [tunnel providers](https://github.com/anderspitman/awesome-tunneling) to make your local instance accessible from the internet. ### Configure webhook in Cohesivo In `config/packages/ibexa_connector_quable.yaml`, specify the configuration for the Quable connector: ```yaml ibexa_connector_quable: # ... webhook_secret: '' ``` > **Caution: Caution** > > [Quable uses dynamic IP addresses](https://faq.quable.com/en/articles/8250056-what-are-the-ip-addresses-of-quable-to-add-to-the-whitelist) to connect to Cohesivo. If your Cohesivo instance is protected by a firewall, make sure your configuration allows connections from changing IP addresses. ### Configure background task Cohesivo webhook processes Quable's classification change events and queues them to be processed in the background. To process them, [configure Ibexa Messenger](https://doc.ibexa.co/en/saas/infrastructure_and_maintenance/background_tasks/index.md) and make sure the `messenger:consume` command is run periodically. # Configure Quable connector > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). Quable connector configuration reference for Cohesivo You can customize the behavior of the Quable integration add-on by using the following [configuration](https://doc.ibexa.co/en/saas/administration/configuration/configuration/index.md). ## Configuration example In `config/packages/ibexa_connector_quable.yaml`, specify your configuration by using the `ibexa_connector_quable` key: ```yaml ibexa_connector_quable: enabled: true instance_url: 'https://example.quable.com' api_token: '' channel_code: '' webhook_secret: '' # Needed for webhook authentication language_map: eng-GB: en_GB fre-FR: fr_FR throw_on_invalid_criteria: '%kernel.debug%' throw_on_invalid_mapping: '%kernel.debug%' cache: enabled: true attribute: true attribute_group: true product: true product_type: true ``` ## Configuration options | Parameter | Default value | Description | | --------------------------- | ---------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `enabled` | `false` | Enables the connector. | | `instance_url` | string | Base URL of your Quable instance, for example `https://example.quable.com`. | | `api_token` | string | [Read Access API token](https://docs.quable.com/v5-EN/docs/api-tokens) used to authenticate requests to Quable. | | `channel_code` | string | Code of the [Quable channel](https://docs.quable.com/v5-EN/docs/content-channels) used as the source of product data. | | `webhook_secret` | string | Secret expected in the [webhook](https://docs.quable.com/v5-EN/docs/webhook) authorization header. | | `language_map` | Empty | Maps Cohesivo language codes (for example, `eng-GB`) to Quable locale codes (for example, `en_GB`). For more information, see [Set up Quable languages](https://doc.ibexa.co/en/saas/product_catalog/quable/install_quable/#set-up-languages). | | `throw_on_invalid_criteria` | `%kernel.debug%` | Controls behavior for unsupported search criteria: `true` throws an exception, `false` only logs unsupported criteria. | | `throw_on_invalid_mapping` | `%kernel.debug%` | Controls behavior for mapping errors during data transformation: `true` throws an exception, `false` only logs mapping errors. | | `cache.enabled` | `true` | Global cache switch for the connector. When set to `false`, only [in-memory cache](https://doc.ibexa.co/en/saas/infrastructure_and_maintenance/cache/persistence_cache/#in-memory-cache-configuration) is used. When set to `true`, [Symfony's `cache.app` cache pool](https://symfony.com/doc/7.4/cache.html#system-cache-and-application-cache) is used. | | `cache.attribute` | `true` | Enables caching for attribute definition requests. | | `cache.` `attribute_group` | `true` | Enables caching for attribute group requests. | | `cache.` `product` | `true` | Enables caching for product requests. | | `cache.` `product_type` | `true` | Enables caching for product type requests. | In production environments, it's recommended to: - keep the `api_token` and the `webhook_secret` [secure](https://doc.ibexa.co/en/saas/infrastructure_and_maintenance/security/security_checklist/#app_secret-and-other-secrets) - enable caching for better performance, by using Redis or Valkey as [persistence cache](https://doc.ibexa.co/en/saas/infrastructure_and_maintenance/cache/persistence_cache/#redisvalkey) - disable `throw_on_invalid_criteria` and `throw_on_invalid_mapping` to prevent non-critical errors from causing application crashes # Quable API > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). Learn how to use PHP and REST APIs to retrieve product data from Quable As Quable products are represented as Cohesivo products, you can use the existing [Product APIs](https://doc.ibexa.co/en/saas/product_catalog/product_api/index.md) to retrieve the product information. Quable is the source of truth about products and categories and you should only use the Cohesivo APIs to read the information coming from Quable, but you can't use them to modify it. To modify the information, use the [Quable interface](https://www.quable.com) or the dedicated [Quable APIs](https://developers.quable.com/quable-api/). ## REST API Usage To learn how to work with Cohesivo REST API, see [REST API reference](https://doc.ibexa.co/en/saas/api/rest_api/rest_api_usage/rest_api_usage/index.md). You can use the following endpoints to retrieve product and category information: - [Product REST API](https://doc.ibexa.co/en/saas/api/rest_api/rest_api_reference/rest_api_reference.html#tag/Product) - [Taxonomy REST API](https://doc.ibexa.co/en/saas/api/rest_api/rest_api_reference/rest_api_reference.html#tag/Taxonomy) ## PHP API Usage ### Retrieve products To retrieve product information coming from Quable, use the same APIs as described in [Product API](https://doc.ibexa.co/en/saas/product_catalog/product_api/index.md). The following example shows how you can retrieve a single product: ```php $product = $this->productService->getProduct($productCode); ``` ### Search for products Use [`ProductQuery`](https://doc.ibexa.co/en/saas/product_catalog/product_api/#getting-product-information) to search for multiple products: ```php $criteria = new Criterion\ProductType([$productType]); $sortClauses = [new SortClause\ProductName(ProductQuery::SORT_ASC)]; $productQuery = new ProductQuery(null, $criteria, $sortClauses); $products = $this->productService->findProducts($productQuery); foreach ($products as $product) { $output->writeln($product->getName() . ' of type ' . $product->getProductType()->getName()); } ``` When working with Quable products, the following search criteria are supported: | Search Criterion | Search based on | | -------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------- | | [CreatedAt](https://doc.ibexa.co/en/saas/search/criteria_reference/createdat_criterion/index.md) | Date and time when product was created | | [LogicalAnd](https://doc.ibexa.co/en/saas/search/criteria_reference/logicaland_criterion/index.md) | Composite criterion combining multiple criteria with AND | | [MatchAll](https://doc.ibexa.co/en/saas/search/criteria_reference/matchall_criterion/index.md) | All products | | [ProductCategory](https://doc.ibexa.co/en/saas/search/criteria_reference/productcategory_criterion/index.md) | Product category assigned to product | | [ProductCategorySubtree](https://doc.ibexa.co/en/saas/search/criteria_reference/productcategorysubtree_criterion/index.md) | Product category subtree | | [ProductCode](https://doc.ibexa.co/en/saas/search/criteria_reference/productcode_criterion/index.md) | Product's code | | [ProductName](https://doc.ibexa.co/en/saas/search/criteria_reference/productname_criterion/index.md) | Product's name | | [ProductType](https://doc.ibexa.co/en/saas/search/criteria_reference/producttype_criterion/index.md) | Product type | | [UpdatedAt](https://doc.ibexa.co/en/saas/search/criteria_reference/updated_at_criterion/index.md) | Date and time when product was last updated | The following sort clauses are supported: | Sort Clause | Sorting based on | | --------------------------------------------------------------------------------------------------------- | ------------------------------------------ | | [CreatedAt](https://doc.ibexa.co/en/saas/search/sort_clause_reference/createdat_sort_clause/index.md) | Date and time of the creation of a product | | [ProductCode](https://doc.ibexa.co/en/saas/search/sort_clause_reference/productcode_sort_clause/index.md) | Product's code | | [ProductName](https://doc.ibexa.co/en/saas/search/sort_clause_reference/productname_sort_clause/index.md) | Product's name | ### Manage stock and pricing For information stored outside of Quable, such as [product availability](https://doc.ibexa.co/en/saas/product_catalog/product_api/#product-availability) or [pricing](https://doc.ibexa.co/en/saas/product_catalog/price_api/index.md), you can use the existing services to manage them: ```php // Manage availability $product = $this->productService->getProduct('NEWMODIFIEDPRODUCT'); $productAvailabilityCreateStruct = new ProductAvailabilityCreateStruct($product, false, true); $this->productAvailabilityService->createProductAvailability($productAvailabilityCreateStruct); // Manage prices $newCurrency = $this->currencyService->getCurrencyByCode($newCurrencyCode); $money = new Money\Money(50000, new Money\Currency($newCurrencyCode)); $priceCreateStruct = new ProductPriceCreateStruct($product, $newCurrency, $money, null, null); $this->productPriceService->createProductPrice($priceCreateStruct); ``` # Product catalog configuration > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). Configure product catalog settings per repository, with different catalog engines and VAT configurations. You can configure the product catalog per [Repository](https://doc.ibexa.co/en/saas/administration/configuration/repository_configuration/index.md). Under `ibexa.repositories..product_catalog` [configuration key](https://doc.ibexa.co/en/saas/administration/configuration/configuration/#configuration-files), indicate the catalog engine to use: ```yaml ibexa: repositories: default: storage: ~ search: engine: '%search_engine%' connection: default product_catalog: engine: 'default' ``` The `default` engine is available out of the box, and configured under `ibexa_product_catalog`: ```yaml ibexa_product_catalog: engines: default: type: local options: root_location_remote_id: e5ce2e391bd94e26a5cd88746f24ecce product_type_group_identifier: 'product' ``` The `local` type is the built-in type of catalog based on the content repository. With [Quable integration](https://doc.ibexa.co/en/saas/product_catalog/quable/quable_guide/index.md) add-on installed and configured, by using the `quable` type you can retrieve product data coming from Quable. You can use a single engine across all repositories, or assign different ones per repository. Each repository can use only one product catalog engine. Under `options.product_type_group_identifier` you can define the identifier of the content type Group used for storing products. `root_location_remote_id` indicates the remote ID of the location where products are stored. ## VAT rates To set up different VAT rates for different regions (countries), you can use the following configuration under the `ibexa.repositories..product_catalog.regions` [configuration key](https://doc.ibexa.co/en/saas/administration/configuration/configuration/#configuration-files), for example: ```yaml ibexa: repositories: : product_catalog: engine: default regions: : vat_categories: standard: value: 18 extras: : reduced: value: 6 zero: value: 0 none: value: ~ ``` VAT rates configuration accepts additional flags under the `extras` key. It's an extension point that you can build upon to add custom functionalities. You can use it, for example, to pass additional information to the UI or define region-specific exclusions when calculating the tax values. For each VAT category value, setting a value to "null" (~) is equal to making the following setting: ```yaml none: value: 0 extras: not_applicable: true ``` ## Code generation strategy Product codes for variants are generated automatically based on the selected strategy. The following strategies are available: - `incremental` (default) - variant code consists of base product code plus index, for example: `ErgoDesk-1`, `ErgoDesk-2`. - `random` - variant code consists of base product code plus random string of characters, for example: `ErgoDesk-62E7B3379AEB4`, `ErgoDesk-62E7B3379AFBC` You can choose the strategy with the following configuration: ```yaml ibexa_product_catalog: engines: default: type: local options: root_location_remote_id: ibexa_product_catalog_root product_type_group_identifier: 'product' variant_code_generator_strategy: 'random' ``` You can also [create your own custom code generation strategy](https://doc.ibexa.co/en/saas/product_catalog/create_product_code_generator/index.md). ## Catalogs ### Catalog filters You can configure which [catalog filters](https://doc.ibexa.co/en/saas/product_catalog/catalogs/index.md) are applied by default with the following configuration: ```yaml ibexa: system: admin: product_catalog: catalogs: default_filters: - product_code - product_availability ``` The order of filters in this configuration reflects the order in which they're displayed in the back office. # Products > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). Products are characterized by attributes describing their characteristics. You can create product variants and add assets to each product and variant. Products are a special type of content that contains typical content Fields and additional product information. Each product belongs to a product type (similar to how a content item belongs to a content type). Each product has a unique identifying product code. Product code can have up to 64 characters. It can contain only letters, numbers, underscores, and dashes. ## Product types Product types represent categories that a product can belong to. A product type can be, for example, a sofa, or a keyboard. Product types, like content types, define the global properties of products and fields a product consists of. A product type also defines the attributes that all products of this type can have. You can choose between two available types: `physical` and `virtual`: - `physical` - tangible products with assigned stock. They can use measurement attributes. They require shipment in the online purchase process. Examples: heaters, laptops, phones. - `virtual` - non-tangible items. They can be sold individually, or as part of a product bundle. They don't require shipment in the online process. Examples: memberships, services, warranties. ## Product attributes Product attributes provide different information about a product and can be used to create [product variants](#product-variants). Typical product attribute examples are: length, weight, color, format, and more. The following attribute types are available: | Name | Identifier | Description | | ------------------------------------------------------------------------------------------------ | ----------- | ------------------------------------------------------------------------------------------------ | | Checkbox | `checkbox` | Boolean attribute with a true/false value. | | Color | `color` | Color value stored as a hex code. | | [Date and time](https://doc.ibexa.co/en/saas/product_catalog/attributes/date_and_time/index.md) | `datetime` | Date and time value with configurable accuracy levels. | | Float | `float` | Decimal number value. | | Integer | `integer` | Integer number value. | | Selection | `selection` | A value selected from a predefined list of labeled options. | | [Symbol](https://doc.ibexa.co/en/saas/product_catalog/attributes/symbol_attribute_type/index.md) | `symbol` | String value with an enforced format, suitable for standardized identifiers such as EAN or ISBN. | Product attributes are collected in groups. An example of an attribute group can be dimensions (length, width, height). You can assign both whole attribute groups or individual attributes to a product type. > **Note: Attribute translations** > > Product attributes are not translatable. Unlike content fields, product attribute values cannot differ between languages. > > For the information that is intended to be displayed, consider using [TextLine](https://doc.ibexa.co/en/saas/content_management/field_types/field_type_reference/textlinefield/index.md) fields for short text, [RichText](https://doc.ibexa.co/en/saas/content_management/field_types/field_type_reference/richtextfield/index.md) fields for longer text that may require formatting, and product attributes for precise product properties or specifications. ## Product variants Product variants represent different versions of a product, for example, clothes in different colors, or laptops with different amounts of RAM. You can create product variants automatically based on attributes that have the "Used for product variants" flag enabled in the product type definition. You can create variants for any combination of values of selected attributes. In the back office you can automatically generate all possible variants for a product. Codes for product variants are generated automatically based on the [selected strategy](https://doc.ibexa.co/en/saas/product_catalog/product_catalog_configuration/#code-generation-strategy). Each product variant has separate availability and stock information. Each variant can also have separate price rules. If a variant doesn't have separate price rules, it uses the price of its base product. ## Product assets Product assets are images that are assigned to products and their specific variants. You can group assets in collections which correspond to specific values of attributes. A collection is assigned to the variant or variants that have these attribute values. ## Embed products in content You can embed products directly into content, including the [landing pages](https://doc.ibexa.co/en/saas/content_management/pages/pages/index.md), by using the [Online Editor](https://doc.ibexa.co/en/saas/content_management/rich_text/online_editor_guide/index.md). Use it to build marketing campaigns directly around the products, bridging product marketing and product data together. To customize the design of the embedded products, see [Customize product embed templates](https://doc.ibexa.co/en/saas/product_catalog/customize_product_embed_templates/index.md). ## Product availability and stock Product availability defines whether a product is available in the catalog. You set product availability per variant or per base product: - if a product cannot have variants (has no attributes with the "Used for product variants" flag), you set availability per base product - if a product can have variants (even if no variants are configured yet), you set availability per variant. When a product is set as available, it can have numerical stock defined. The stock can also be set to infinite (for example, in case of digital products). ### Availability and computed availability Setting a product as available doesn't automatically mean that it can be ordered. For example, a product can be set as available, but have zero stock. The product catalog distinguishes between two types of availability: - Availability as a value set per product or variant Availability represents whether the product was set as **Available**, for example in the [back office **Availability** tab](https://doc.ibexa.co/projects/userguide/en/6.0/product_catalog/manage_availability_and_stock/#set-product-availability) or [PHP API](https://doc.ibexa.co/en/saas/product_catalog/product_api/#product-availability). - Computed availability Computed availability represents whether the product can actually be ordered. By default, a product can only be ordered when it's set as available and has either positive or infinite stock. You can implement a custom strategy to handle different selling scenarios, such as minimum order quantity, minimum stock quantity, or region-specific availability. For more information, see [Create custom availability strategy](https://doc.ibexa.co/en/saas/product_catalog/create_custom_availability_strategy/index.md). # Date and time attributes > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). Date and time attribute type allows you to store product information related to time, like an expiration date or date of manufacturing. The date and time [attribute type](https://doc.ibexa.co/en/saas/product_catalog/products/#product-attributes) allows you to represent date and time values as part of the product specification in the [product catalog](https://doc.ibexa.co/en/saas/product_catalog/product_catalog_guide/index.md). You can use it to store, for example, manufacturing dates, expiration dates, or event dates, all with specified accuracy. ## Usage You can manage the date and time attribute type through the back office, [data migrations](https://doc.ibexa.co/en/saas/content_management/data_migration/importing_data/#date-and-time-attributes), REST, or through the PHP API. It also supports [searching](https://doc.ibexa.co/en/saas/search/criteria_reference/product_search_criteria/index.md) by using [DateTimeAttribute](https://doc.ibexa.co/en/saas/search/criteria_reference/datetimeattribute_criterion/index.md) and [DateTimeAttributeRange](https://doc.ibexa.co/en/saas/search/criteria_reference/datetimeattributerange_criterion/index.md) criteria. !\[Creating a product using a date and time attribute with "trimester" accuracy level\](../img/datetime.png "Creating a product using a date and time attribute with "trimester" accuracy level") When creating an attribute based on the date and time attribute type you can select the accuracy level to match your needs: | Accuracy | Example | Limitations | | --------- | ------------------- | ---------------------------- | | Year | 2025 | Number between 1000 and 9999 | | Trimester | Q3 2025 | | | Month | July 2025 | | | Day | 2025-07-06 | | | Minute | 2025-07-06 11:15 | | | Second | 2025-07-06 11:15:37 | | # Symbol attribute type > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). Create a symbol attribute type that enables for the efficient representation of string-based values while enforcing their format in product specifications. In product specifications, the symbol attribute type enables the efficient representation of string-based data and enforces their format. This feature allows you to store standard product identifiers (such as EAN or ISBN) in the [product catalog](https://doc.ibexa.co/en/saas/product_catalog/product_catalog_guide/index.md). ## Build-in symbol attribute formats The built-in symbol attribute formats in `ibexa/product-catalog-symbol-attribute` are listed below: | Name | Description | Example | | -------------------------------------- | ----------------------------------------------------------------------------------------- | ----------------- | | Generic | Accepts any string value | #FR1.2 | | Generic (alphabetic characters only) | Accepts any string value that contains only letters | ABCD | | Generic (digits only) | Accepts any string value that contains only digits | 123456 | | Generic (alphanumeric characters only) | Accepts any string value that contains only letters or digits | 2N6405G | | Generic (hexadecimal digits only) | Accepts any string value that contains only hexadecimal digits (digits or A-F characters) | DEADBEEF | | EAN-8 | European Article Number (8 characters) | 96385074 | | EAN-13 | European Article Number (13 characters) | 5023920187205 | | EAN-14 | European Article Number (14 characters) | 12345678901231 | | ISBN-10 | International Standard Book Number (10 characters) | 0-19-852663-6 | | ISBN-13 | International Standard Book Number (13 characters) | 978-1-86197-876-9 | > **Caution: Caution** > > Maximum length of the symbol value is 160 characters. ## Create custom symbol attribute format Under the `ibexa_product_catalog_symbol_attribute.formats` key, you can use configuration to create your own symbol format. See the example below: ```yaml ibexa_product_catalog_symbol_attribute: formats: manufacturer_part_number: name: 'Manufacturer Part Number' pattern: '/^[A-Z]{3}-\d{5}$/' examples: - 'RPI-14645' - 'MSS-24827' - 'SEE-15444' ``` This following example specifies the format for a "Manufacturer Part Number", defined with the `manufacturer_part_number` identifier. The pattern is specified using a regular expression. According to the pattern option, the attribute value: - must be a string - begins with three capital letters (A-Z), followed by a hyphen ("-") - ends with five digits (0-9), with no other characters before or after Certain formats, such as the International Standard Book Number (ISBN-10) and the European Article Number (EAN-13), contain checksum digits and are self-validating. To validate checksum of symbol: 1. Create a class implementing the [`\Ibexa\Contracts\ProductCatalogSymbolAttribute\Value\ChecksumInterface`](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-ProductCatalogSymbolAttribute-Value-ChecksumInterface.html) interface. 2. Register the class as a service using the `ibexa.product_catalog.attribute.symbol.checksum` tag and specify the format identifier using the `format` attribute. See below the example implementation of checksum validation using Luhn formula: ```php getDigits($value); $count = count($digits); $total = 0; for ($i = $count - 2; $i >= 0; $i -= 2) { $digit = $digits[$i]; if ($i % 2 === 0) { $digit *= 2; } $total += $digit > 9 ? $digit - 9 : $digit; } $checksum = $digits[$count - 1]; return $total + $checksum === 0; } /** * Returns an array of digits from the given value (skipping any formatting characters). * * @return int[] */ private function getDigits(string $value): array { $chars = array_filter( str_split($value), static fn (string $char): bool => $char !== '-' ); return array_map(intval(...), array_values($chars)); } } ``` Example service definition: ```yaml services: App\PIM\Symbol\Format\Checksum\LuhnChecksum: tags: - name: ibexa.product_catalog.attribute.symbol.checksum format: my_format ``` The format attribute (`my_format`) is the identifier used under the `ibexa_product_catalog_symbol_attribute.formats` key. ## Search for products with given symbol attribute You can use `SymbolAttribute` Search Criterion to find products by symbol attribute: For more information, see [SymbolAttribute Criterion](https://doc.ibexa.co/en/saas/search/criteria_reference/symbolattribute_criterion/index.md). # Product API > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). Use PHP API to manage products, their attributes, availability, and prices. ## Products Cohesivo's Product API provides two services for handling product information, which differ in function: | Service name | Description | | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | [`ProductServiceInterface`](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-ProductCatalog-ProductServiceInterface.html) | Use it to retrieve product data regardless of the source: Cohesivo, [Quable](https://doc.ibexa.co/en/saas/product_catalog/quable/quable/index.md), or [remote PIM](https://doc.ibexa.co/en/saas/product_catalog/add_remote_pim_support/index.md) | | [`LocalProductServiceInterface`](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-ProductCatalog-Local-LocalProductServiceInterface.html) | Use it to modify products defined in Cohesivo | > **Tip: Product REST API** > > To learn how to load products using the REST API, see [REST API reference](https://doc.ibexa.co/en/saas/api/rest_api/rest_api_reference/rest_api_reference.html#tag/Product/operation/api_productcatalogproductsview_post). ### Getting product information Get an individual product by using the `ProductServiceInterface::getProduct()` method: ```php $product = $this->productService->getProduct($productCode); $output->writeln('Product with code ' . $product->getCode() . ' is ' . $product->getName()); ``` Find multiple products with `ProductServiceInterface::findProducts()`. Provide the method with optional filter, query or Sort Clauses. ```php $criteria = new Criterion\ProductType([$productType]); $sortClauses = [new SortClause\ProductName(ProductQuery::SORT_ASC)]; $productQuery = new ProductQuery(null, $criteria, $sortClauses); $products = $this->productService->findProducts($productQuery); foreach ($products as $product) { $output->writeln($product->getName() . ' of type ' . $product->getProductType()->getName()); } ``` See [Product Search Criteria](https://doc.ibexa.co/en/saas/search/criteria_reference/product_search_criteria/index.md) and [Product Sort Clauses](https://doc.ibexa.co/en/saas/search/sort_clause_reference/product_sort_clauses/index.md) references for more information about how to use the [`ProductQuery`](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-ProductCatalog-Values-Product-ProductQuery.html) class. ### Modifying products To create, update and delete products, use the `LocalProductServiceInterface`. ```php $productUpdateStruct = $this->localProductService->newProductUpdateStruct($product); $productUpdateStruct->setCode('NEWMODIFIEDPRODUCT'); $this->localProductService->updateProduct($productUpdateStruct); ``` To create a product, use `LocalProductServiceInterface::newProductCreateStruct()` to get a [`ProductCreateStruct`](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-ProductCatalog-Local-Values-Product-ProductCreateStruct.html). Provide the method with the product type object and the main language code. You also need to set (at least) the code for the product and the required Field of the underlying content type, `name`: ```php $productType = $this->productTypeService->getProductType($productType); $createStruct = $this->localProductService->newProductCreateStruct($productType, 'eng-GB'); $createStruct->setCode('NEWPRODUCT'); $createStruct->setField('name', 'New Product'); $this->localProductService->createProduct($createStruct); ``` To delete a product, use `LocalProductServiceInterface::deleteProduct()`: ```php $this->localProductService->deleteProduct($product); ``` ### Product variants #### Searching for variants of a specific product You can access the variants of a product by using the [`ProductServiceInterface::findProductVariants()`](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-ProductCatalog-ProductServiceInterface.html#method_findProductVariants) method. The method takes the product object and a [`ProductVariantQuery`](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-ProductCatalog-Values-Product-ProductVariantQuery.html) object as parameters. You can filter variants by: - variant codes: ```php // Get variants filtered by variant codes $codeQuery = new ProductVariantQuery(); $codeQuery->setVariantCodes(['DESK-red', 'DESK-blue']); $specificVariants = $this->productService->findProductVariants($product, $codeQuery)->getVariants(); ``` - product criteria: To use [Product Search Criteria](https://doc.ibexa.co/en/saas/search/criteria_reference/product_search_criteria/index.md) with [`ProductVariantQuery`](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-ProductCatalog-Values-Product-ProductVariantQuery.html), wrap it with the [`ProductCriterionAdapter`](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-ProductCatalog-Values-Content-Query-Criterion-ProductCriterionAdapter.html) class, as in the example below: ```php // Get variants with specific attributes $combinedQuery = new ProductVariantQuery(); $combinedQuery->setAttributesCriterion( new ProductCriterionAdapter( new Criterion\LogicalAnd([ new Criterion\ColorAttribute('color', ['red', 'blue']), new Criterion\IntegerAttribute('size', 42), ]) ) ); $filteredVariants = $this->productService->findProductVariants($product, $combinedQuery)->getVariants(); ``` From a variant ([`ProductVariantInterface`](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-ProductCatalog-Values-ProductVariantInterface.html)), you can access the attributes that are used to generate the variant by using the [`ProductVariantInterface::getDiscriminatorAttributes()`](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-ProductCatalog-Values-ProductVariantInterface.html#method_getDiscriminatorAttributes) method. ```php $attributes = $variant->getDiscriminatorAttributes(); foreach ($attributes as $attribute) { $output->writeln($attribute->getIdentifier() . ': ' . $attribute->getValue() . ' '); } ``` #### Searching for variants across all products To search for variants across all products, use the [`ProductServiceInterface::findVariants()`](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-ProductCatalog-ProductServiceInterface.html#method_findVariants) method. This method takes a [`ProductVariantQuery`](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-ProductCatalog-Values-Product-ProductVariantQuery.html) object and returns variants regardless of their base product. Unlike `findProductVariants()`, which requires a specific product object, `findVariants()` allows you to search the entire variant catalog. You can filter variants by: - variant codes: ```php // Search variants across all products $query = new ProductVariantQuery(); $query->setVariantCodes(['DESK-red', 'DESK-blue']); $variantList = $this->productService->findVariants($query); ``` - product criteria: To use [Product Search Criteria](https://doc.ibexa.co/en/saas/search/criteria_reference/product_search_criteria/index.md) with [`ProductVariantQuery`](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-ProductCatalog-Values-Product-ProductVariantQuery.html), wrap it with the [`ProductCriterionAdapter`](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-ProductCatalog-Values-Content-Query-Criterion-ProductCriterionAdapter.html) class, as in the example below: ```php // Search variants with attribute criterion $colorQuery = new ProductVariantQuery(); $colorQuery->setAttributesCriterion( new ProductCriterionAdapter( new Criterion\ColorAttribute('color', ['red']) ) ); $redVariants = $this->productService->findVariants($colorQuery); ``` #### Creating variants To create a product variant, use `LocalProductServiceInterface::createProductVariants()`. This method takes the product and an array of [`ProductVariantCreateStruct`](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-ProductCatalog-Local-Values-Product-ProductVariantCreateStruct.html) objects as parameters. `ProductVariantCreateStruct` specifies the attribute values and the code for the new variant. ```php $query->setVariantCodes(['DESK-red', 'DESK-blue']); $variantList = $this->productService->findVariants($query); foreach ($variantList->getVariants() as $variant) { $output->writeln($variant->getName()); } ``` ### Product assets You can get assets assigned to a product by using [`AssetServiceInterface`](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-ProductCatalog-AssetServiceInterface.html). Use `AssetServiceInterface` to get a single asset by providing the product object and the assets's ID as parameters: ```php $singleAsset = $this->assetService->getAsset($product, '1'); $output->writeln($singleAsset->getName()); ``` To get all assets assigned to a product, use `AssetServiceInterface::findAssets()`. You can retrieve the tags (corresponding to attribute values) of assets with the `AssetInterface::getTags()` method: ```php $assetCollection = $this->assetService->findAssets($product); foreach ($assetCollection as $asset) { $output->writeln($asset->getIdentifier() . ': ' . $asset->getName()); $tags = $asset->getTags(); foreach ($tags as $tag) { $output->writeln($tag); } } ``` ## Product types To work with product types, use [`ProductTypeServiceInterface`](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-ProductCatalog-ProductTypeServiceInterface.html). ### Creating product types To create a product type, use [`LocalProductTypeServiceInterface`](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-ProductCatalog-Local-LocalProductTypeServiceInterface.html). First, create a product type struct with `LocalProductTypeServiceInterface::newProductTypeCreateStruct()`, providing the identifier and main language code: ```php $productTypeCreateStruct = $this->localProductTypeService->newProductTypeCreateStruct( 'digital_product', 'eng-GB' ); ``` You can set names in multiple languages by using `setNames()`: ```php $productTypeCreateStruct->setNames([ 'eng-GB' => 'Digital Product', 'pol-PL' => 'Produkt Cyfrowy', ]); ``` To create a virtual product type (for products that don't require shipping), use `setVirtual()`: ```php $productTypeCreateStruct->setVirtual(true); ``` #### Adding field definitions To add custom field definitions to the product type, use `getContentTypeCreateStruct()` to access the underlying content type struct. For more information about working with content types, see [Adding content types](https://doc.ibexa.co/en/saas/content_management/content_api/managing_content/#adding-content-types). ```php $marketingDescriptionFieldDefinition = $this->contentTypeService->newFieldDefinitionCreateStruct( 'marketing_description', 'ibexa_string' ); $marketingDescriptionFieldDefinition->names = ['eng-GB' => 'Marketing Description']; $marketingDescriptionFieldDefinition->position = 100; $contentTypeCreateStruct->addFieldDefinition($marketingDescriptionFieldDefinition); ``` #### Assigning attributes To assign product attributes to the product type, use `setAssignedAttributesDefinitions()` with an array of [`AssignAttributeDefinitionStruct`](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-ProductCatalog-Local-Values-ProductType-AssignAttributeDefinitionStruct.html) objects. First, retrieve the attribute definition by using [`AttributeDefinitionServiceInterface`](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-ProductCatalog-AttributeDefinitionServiceInterface.html): ```php $sizeAttribute = $this->attributeDefinitionService->getAttributeDefinition('size'); ``` Then create the assignment struct with the attribute definition, and set whether it's required and whether it's a discriminator (used for product variants): ```php $attributeAssignment = new AssignAttributeDefinitionStruct( $sizeAttribute, false, false ); $productTypeCreateStruct->setAssignedAttributesDefinitions([$attributeAssignment]); ``` For more information about working with attributes through PHP API, see [Attributes](#attributes). #### Storing new product type Finally, create the product type with `LocalProductTypeServiceInterface::createProductType()`: ```php $newProductType = $this->localProductTypeService->createProductType($productTypeCreateStruct); ``` ### Getting product types Get a product type object by using `ProductTypeServiceInterface::getProductType()`: ```php $productType = $this->productTypeService->getProductType($productTypeIdentifier); ``` You can also get a list of product types with `ProductTypeServiceInterface::findProductTypes()`: ```php $productTypes = $this->productTypeService->findProductTypes(); foreach ($productTypes as $productType) { $output->writeln($productType->getName() . ' with identifier ' . $productType->getIdentifier()); } ``` ## Product availability Product availability is an object which defines whether a product is set as available, in what stock, and whether it can be ordered. To manage it, use [`ProductAvailabilityServiceInterface`](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-ProductCatalog-ProductAvailabilityServiceInterface.html). The [`AvailabilityInterface`](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-ProductCatalog-Values-Availability-AvailabilityInterface.html) provides two distinct availability values: - `getAvailability()` returns the value of availability flag as set for the product - `getComputedAvailability()` returns whether the product can be ordered For more information about the distinction between these two values, see [Availability and computed availability](https://doc.ibexa.co/en/saas/product_catalog/products/#availability-and-computed-availability). To check whether a product is set as available, use `ProductAvailabilityServiceInterface::hasAvailability()`. You can get the availability object with `ProductAvailabilityServiceInterface::getAvailability()`. The returned object contains both the stored and computed availability: ```php if ($this->productAvailabilityService->hasAvailability($product)) { $availability = $this->productAvailabilityService->getAvailability($product); $output->writeln($availability->getAvailability() ? 'Available flag: true' : 'Available flag: false'); $output->writeln($availability->getComputedAvailability() ? 'Can be ordered: true' : 'Can be ordered: false'); $output->writeln('Stock: ' . $availability->getStock()); } ``` To evaluate computed availability for a [specific context](https://doc.ibexa.co/en/saas/product_catalog/create_custom_availability_strategy/index.md), for example, a specific requested quantity or customer group, pass an optional [`AvailabilityContextInterface`](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-ProductCatalog-Values-Availability-AvailabilityContextInterface.html) object as the second argument: ```php $availability = $this->productAvailabilityService->getAvailability( $product, new PurchasableWithoutStockAvailabilityContext() ); $canBeOrdered = $availability->getComputedAvailability(); $output->writeln('Can be ordered: ' . ($canBeOrdered ? 'true' : 'false') . ', Stock: ' . $availability->getStock()); ``` To change availability for a product, use `ProductAvailabilityServiceInterface::updateProductAvailability()` with a [`ProductAvailabilityUpdateStruct`](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-ProductCatalog-Values-Availability-ProductAvailabilityUpdateStruct.html) and provide it with the product object. The second parameter defines whether product is available, and the third whether its stock is infinite. The fourth parameter is the stock number: ```php $productAvailabilityUpdateStruct = new ProductAvailabilityUpdateStruct($product, true, false, 80); $this->productAvailabilityService->updateProductAvailability($productAvailabilityUpdateStruct); ``` ## Attributes To get information about product attribute groups, use the [`AttributeGroupServiceInterface`](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-ProductCatalog-AttributeGroupServiceInterface.html), or [`LocalAttributeGroupServiceInterface`](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-ProductCatalog-Local-LocalAttributeGroupServiceInterface.html) to modify attribute groups. `AttributeGroupServiceInterface::getAttributeGroup()` enables you to get a single attribute group by its identifier. `AttributeGroupServiceInterface::findAttributeGroups()` gets attribute groups, all of them or filtered with an optional [`AttributeGroupQuery`](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-ProductCatalog-Values-AttributeGroup-AttributeGroupQuery.html) object: ```php $attributeGroup = $this->attributeGroupService->getAttributeGroup('dimensions'); $attributeGroups = $this->attributeGroupService->findAttributeGroups(); foreach ($attributeGroups as $attributeGroup) { $output->writeln('Attribute group ' . $attributeGroup->getIdentifier() . ' with name ' . $attributeGroup->getName()); } ``` To create an attribute group, use `LocalAttributeGroupServiceinterface::createAttributeGroup()` and provide it with an [`AttributeGroupCreateStruct`](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-ProductCatalog-Local-Values-AttributeGroup-AttributeGroupCreateStruct.html): ```php $attributeGroupCreateStruct = $this->localAttributeGroupService->newAttributeGroupCreateStruct('dimensions'); $attributeGroupCreateStruct->setNames(['eng-GB' => 'Size']); $this->localAttributeGroupService->createAttributeGroup($attributeGroupCreateStruct); ``` To get information about product attributes, use the [`AttributeDefinitionServiceInterface`](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-ProductCatalog-AttributeDefinitionServiceInterface.html), or [`LocalAttributeDefinitionServiceInterface`](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-ProductCatalog-Local-LocalAttributeDefinitionServiceInterface.html) to modify attributes. ```php $attribute = $this->attributeDefinitionService->getAttributeDefinition('length'); $output->writeln($attribute->getName()); ``` To create an attribute, use `LocalAttributeGroupServiceinterface::createAttributeDefinition()` and provide it with an [`AttributeDefinitionCreateStruct`](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-ProductCatalog-Local-Values-AttributeDefinition-AttributeDefinitionCreateStruct.html): ```php $attributeCreateStruct = $this->localAttributeDefinitionService->newAttributeDefinitionCreateStruct('size'); $attributeCreateStruct->setType($attributeType); $attributeCreateStruct->setName('eng-GB', 'Size'); $attributeCreateStruct->setGroup($attributeGroup); $this->localAttributeDefinitionService->createAttributeDefinition($attributeCreateStruct); ``` # Catalogs > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). Catalogs enable filtering out a selection of products from the Product catalog. You can create multiple catalogs containing subsets of the whole product list. Use them, for example, to build special catalogs for B2B and B2C uses, for retailers and distributors, or for different regions. When creating a catalog, all products are included by default, but you can filter the list by: - price (Solr or Elasticsearch only) - product attributes - product type - product code - availability - product category - the date when the product was created ![List of filters for selecting products for a catalog](https://doc.ibexa.co/en/saas/product_catalog/img/catalogs_filters.png) # Catalog API > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). Use PHP API to get information about and manage product catalogs. To get information about product catalogs and manage them, use `CatalogServiceInterface`. ## Get catalog To get a single catalog, use `Ibexa\Contracts\ProductCatalog\CatalogServiceInterface::getCatalog()` and provide it with catalog ID, or `CatalogServiceInterface::getCatalogByIdentifier()` and pass the identifier: ```php $catalog = $this->catalogService->getCatalogByIdentifier($catalogIdentifier); $output->writeln($catalog->getName()); ``` ## Get products in catalog To get products from a catalog, request the product query from the catalog object with `Ibexa\Contracts\ProductCatalog\Values\CatalogInterface::getQuery()`. Then, create a new `ProductQuery` based on it and run a product search with `ProductServiceInterface::findProduct()`: ```php $productQuery = new ProductQuery(null, $catalog->getQuery()); $products = $this->productService->findProducts($productQuery); foreach ($products as $product) { $output->writeln($product->getName()); } ``` ## Create catalog To create a catalog, you need to prepare a `CatalogCreateStruct` that contains: identifier, name, description, and Criteria for filtering products. Then, pass this struct to `CatalogServiceInterface::createCatalog()`: ```php $catalogCriterion = new Criterion\LogicalAnd( [ new Criterion\ProductType(['desk']), new Criterion\ProductAvailability(true), ] ); $catalogCreateStruct = new CatalogCreateStruct( $catalogIdentifier, $catalogCriterion, ['eng-GB' => 'Desk promo'], ['eng-GB' => 'Desk promo description'], ); $this->catalogService->createCatalog($catalogCreateStruct); ``` ## Update catalog Use `CatalogServiceInterface::updateCatalog()` to update an existing catalog. You must pass the catalog object and a `CatalogUpdateStruct` to the method. In the following example, you update the catalog to publish it: ```php $catalogUpdateStruct = new CatalogUpdateStruct($catalog->getId()); $catalogUpdateStruct->setTransition(Status::PUBLISH_TRANSITION); $this->catalogService->updateCatalog($catalog, $catalogUpdateStruct); ``` # Enable purchasing products > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). Ensure your product catalog is ready for use with full configuration of products that enables purchasing them in the frontend shop. To enable adding product to cart and purchasing from the catalog, the following configuration is required: - at least [one region and one currency for the shop](#region-and-currency) - [VAT rates per region](#vat-rates) and for each product type - at least one [price](https://doc.ibexa.co/en/saas/product_catalog/prices/index.md) for the product - [availability](https://doc.ibexa.co/en/saas/product_catalog/products/#product-availability-and-stock) with positive or infinite stock for the product or product variant > **Note: Configuring products in the UI** > > After you configure the region, currency and VAT rates for regions in settings, the store manager must set up the remaining parameters in the UI, such as, [VAT rates per product type](https://doc.ibexa.co/projects/userguide/en/6.0/product_catalog/create_product_types/#vat), descriptions, attributes, assets, [prices](https://doc.ibexa.co/projects/userguide/en/6.0/product_catalog/manage_prices/), and [availability](https://doc.ibexa.co/projects/userguide/en/6.0/product_catalog/manage_availability_and_stock/) per product. > > For more information, see [User Documentation](https://doc.ibexa.co/projects/userguide/en/6.0/product_catalog/products/#product-completeness). ## Region and currency All currencies available in the system must be enabled in the back office under **Product Catalog** -> **Currencies**. Additionally, you must configure currencies valid for specific SiteAccesses under the `ibexa.system..product_catalog.currencies` [configuration key](https://doc.ibexa.co/en/saas/administration/configuration/configuration/#configuration-files): ```yaml ibexa: system: default: product_catalog: currencies: - EUR - GBP - PLN regions: - germany - uk - poland ``` In the `ibexa_storefront.yaml` file, under the `ibexa.system..product_catalog.regions` configuration key, regions are set with `default` value. Remember to either exclude this element or extend it by [configuring other regions](https://doc.ibexa.co/en/saas/product_catalog/enable_purchasing_products/#configuring-other-regions-and-currencies). ```yaml ibexa: system: storefront_group: product_catalog: currencies: - EUR - PLN regions: - germany - poland another_storefront_group: product_catalog: currencies: - GBP regions: - uk ``` This example uses the currencies and regions set in the [VAT rates' example below](#vat-rates). ### Configuring other regions and currencies By default, the system always uses the first currency and the first region configured. To implement a different logic, for example a switcher for preferred currencies and regions, you need to subscribe to `Ibexa\Contracts\ProductCatalog\Events\CurrencyResolveEvent` and `Ibexa\Contracts\ProductCatalog\Events\RegionResolveEvent` in your customization. ## VAT rates You set up VAT percentage values corresponding to VAT rates in configuration: ```yaml ibexa: repositories: default: product_catalog: engine: default regions: germany: # Shorthand VAT configuration format vat_categories: standard: 19 reduced: 7 none: ~ poland: # Current VAT configuration format vat_categories: standard: value: 23 reduced: value: 8 zero: value: 0 none: value: 0 extras: not_applicable: true ``` > **Note: Note** > > The above example presents two acceptable formats of VAT configuration. For each VAT category, setting a value to "null" (`~`) is equal to making the following setting: > > ```yaml > none: > value: 0 > extras: > not_applicable: true > ``` You can then assign VAT rates that apply to every product type in each of the supported regions. To do it, in the back office, [open the product type for editing](https://doc.ibexa.co/projects/userguide/en/6.0/product_catalog/create_product_types/#vat), and navigate to the **VAT rates** area. ![Assigning VAT rates to a product type](https://doc.ibexa.co/en/saas/product_catalog/img/catalog_vat_rates.png "Assigning VAT rates to a product type") # Prices > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). The price engine calculates product prices taking into account customer groups, currencies and taxes. The price engine is responsible for calculating prices for products in the [product catalog](https://doc.ibexa.co/en/saas/product_catalog/product_catalog/index.md). ## Custom pricing You can set up basic price rules depending on [customer groups](https://doc.ibexa.co/en/saas/users/customer_groups/index.md). Use this option to globally manage custom prices, for example for your resellers. Each customer group can have a default price discount that applies to all products. ### Assign prices dynamically You could create a customer group resolver that provides custom price logic, for example, by retrieving user address from the customer profile, and assigning a customer group to the customer based on the address. Such resolver must implement the [`Ibexa\Contracts\ProductCatalog\CustomerGroupResolverInterface`](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-ProductCatalog-CustomerGroupResolverInterface.html) interface. You must then register it as a service with the `ibexa.product_catalog.customer_group.resolver` tag. ## Currency Cohesivo ships with a list of available currencies, and you can also add custom currencies. To use currencies in your shop, you need to first enable them in the back office. ## VAT You can [configure VAT rate globally](https://doc.ibexa.co/en/saas/product_catalog/product_catalog_configuration/#vat-rates) (per SiteAccess), or set it individually for each product type and product. # Price API > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). Use PHP API to manage currencies in the shop and product prices. ## Currencies To manage currencies, use [`CurrencyServiceInterface`](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-ProductCatalog-CurrencyServiceInterface.html). To access a currency object by its code, use `CurrencyServiceInterface::getCurrencyByCode`. To access a whole list of currencies, use `CurrencyServiceInterface::findCurrencies`. ```php $currency = $this->currencyService->getCurrencyByCode($currencyCode); $output->writeln('Currency ID: ' . $currency->getId()); $currencies = $this->currencyService->findCurrencies(); foreach ($currencies as $currency) { $output->writeln('Currency ' . $currency->getId() . ' with code ' . $currency->getCode()); } ``` To create a new currency, use `CurrencyServiceInterface::createCurrency()` and provide it with a `CurrencyCreateStruct` with code, number of fractional digits and a flag indicating if the currency is enabled: ```php $currencyCreateStruct = new CurrencyCreateStruct($newCurrencyCode, 2, true); $this->currencyService->createCurrency($currencyCreateStruct); ``` ## Prices To manage prices, use [`ProductPriceServiceInterface`](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-ProductCatalog-ProductPriceServiceInterface.html). To retrieve the price of a product in the currency for the current context, use `Product::getPrice()`: ```php $productPrice = $product->getPrice(); $output->writeln('Price for ' . $product->getName() . ' is ' . $productPrice); ``` To retrieve the price of a product in a specific currency, use `ProductPriceService::getPriceByProductAndCurrency`: ```php $productPrice = $this->productPriceService->getPriceByProductAndCurrency($product, $currency); $output->writeln('Price for ' . $product->getName() . ' in ' . $currencyCode . ' is ' . $productPrice); ``` To get all prices (in different currencies) for a given product, use `ProductPriceServiceInterface::findPricesByProductCode`: ```php $prices = $this->productPriceService->findPricesByProductCode($productCode)->getPrices(); $output->writeln('All prices for ' . $product->getName() . ':'); foreach ($prices as $price) { $output->writeln((string) $price); } ``` To load price definitions that match given criteria, use `ProductPriceServiceInterface::findPrices`: ```php use Ibexa\Contracts\ProductCatalog\Values\Price\PriceQuery; use Ibexa\Contracts\ProductCatalog\Values\Price\Query\Criterion\Currency as CurrencyCriterion; use Ibexa\Contracts\ProductCatalog\Values\Price\Query\Criterion\CustomerGroup; use Ibexa\Contracts\ProductCatalog\Values\Price\Query\Criterion\LogicalOr; // ... $priceCriteria = [ new CurrencyCriterion($this->currencyService->getCurrencyByCode('USD')), new CustomerGroup('customer_group_1'), new Product('ergo_desk'), ]; $priceQuery = new PriceQuery(new LogicalOr(...$priceCriteria)); $prices = $this->productPriceService->findPrices($priceQuery); $output->writeln(sprintf('Found %d prices with provided criteria', $prices->getTotalCount())); ``` You can also use `ProductPriceServiceInterface` to create or modify existing prices. For example, to create a new price for a given currency, use `ProductPriceService::createProductPrice` and provide it with a `ProductPriceCreateStruct` object: ```php $newCurrency = $this->currencyService->getCurrencyByCode($newCurrencyCode); $money = new Money\Money(50000, new Money\Currency($newCurrencyCode)); $priceCreateStruct = new ProductPriceCreateStruct($product, $newCurrency, $money, null, null); $this->productPriceService->createProductPrice($priceCreateStruct); ``` > **Note: Note** > > Prices operate using the [`Money`](https://github.com/moneyphp/money) library. That is why all amounts are provided [in the smallest unit](https://www.moneyphp.org/en/stable/getting-started.html#instantiation). For example, for euro `50000` refers to 50000 cents, equal to 500 euros. ### Resolve prices To display a product price on a product page, you must calculate its value based on a base price and the context. Context contains information about any price modifiers that may apply to a specific customer group. To determine the final price, or resolve the price, use the [`PriceResolverInterface`](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-ProductCatalog-PriceResolverInterface.html) service, which takes the following conditions into account: 1. Existence of base price for the product in the specified currency 2. Existence of customer group-related modifiers If the base price in the specified currency is missing, the return value is `null`. To resolve a price of a product in the currency for the current context, use either `PriceResolverInterface::resolvePrice()` or `PriceResolverInterface::resolvePrices()`: ```php use Ibexa\Contracts\ProductCatalog\PriceResolverInterface; use Ibexa\Contracts\ProductCatalog\Values\Price\PriceContext; // ... $context = new PriceContext($currency); $price = $this->priceResolver->resolvePrice($product, $context); $output->writeln('Price in ' . $currency->getCode() . ' for ' . $product->getName() . ' is ' . $price); ``` ## VAT To get information about the VAT categories and rates configured in the system, use [`VatServiceInterface`](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-ProductCatalog-VatServiceInterface.html). VAT is configured per region, so you also need to use [`RegionServiceInterface`](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-ProductCatalog-RegionServiceInterface.html) to get the relevant region object. ```php $region = $this->regionService->getRegion('poland'); ``` To get information about all VAT categories configured for the selected region, use `VatServiceInterface::getVatCategories()`: ```php $vatCategories = $this->vatService->getVatCategories($region); foreach ($vatCategories as $category) { $output->writeln($category->getIdentifier() . ': ' . $category->getVatValue()); } ``` To get a single VAT category, use `VatServiceInterface::getVatCategoryByIdentifier()` and provide it with the region object and the identifier of the VAT category: ```php $vatCategory = $this->vatService->getVatCategoryByIdentifier($region, 'reduced'); ``` # Customize product catalog > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). Customize the product catalog to the needs of your organization. You can customize various areas of the product catalog capabilities to adjust it to the specific requirements of your organization. - [Create custom attribute type](https://ez-systems-developer-documentation--3381.com.readthedocs.build/en/3381/product_catalog/create_custom_attribute_type/): Enhance product catalog by creating a custom product attribute type to fit your specific needs. - [Create custom product code generator strategy](https://ez-systems-developer-documentation--3381.com.readthedocs.build/en/3381/product_catalog/create_product_code_generator/): A custom product code generator enables you to control how product codes are created. - [Create custom catalog filter](https://ez-systems-developer-documentation--3381.com.readthedocs.build/en/3381/product_catalog/create_custom_catalog_filter/): Fine-tune catalogs by adding a custom catalog filter for selecting products from the Product catalog. - [Create custom name schema strategy](https://ez-systems-developer-documentation--3381.com.readthedocs.build/en/3381/product_catalog/create_custom_name_schema_strategy/): Create custom name schema strategy to generate URL aliases based on attribute values. - [Customize product embed templates](https://ez-systems-developer-documentation--3381.com.readthedocs.build/en/3381/product_catalog/customize_product_embed_templates/): Customize the templates used to render products embedded in RichText fields. - [Create custom availability strategy](https://ez-systems-developer-documentation--3381.com.readthedocs.build/en/3381/product_catalog/create_custom_availability_strategy/): Implement custom availability strategies to handle different business scenarios, for example pre-orders or per-region availability. # Create custom attribute type > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). Enhance product catalog by creating a custom product attribute type to fit your specific needs. Besides the [built-in attribute types](https://doc.ibexa.co/en/saas/product_catalog/products/#product-attributes), you can also create custom ones. The example below shows how to add a Percentage attribute type. ## Select attribute type class First, you need to register the type class that the attribute uses: ```yaml services: app.product_catalog.attribute_type.percent: class: Ibexa\ProductCatalog\Local\Repository\Attribute\AttributeType arguments: $identifier: 'percent' tags: - name: ibexa.product_catalog.attribute_type alias: percent ``` Use the `ibexa.product_catalog.attribute_type` tag to indicate the use as a product attribute type. The custom attribute type has the identifier `percent`. ## Create value form mapper A form mapper maps the data entered in an editing form into an attribute value. The form mapper must implement `Ibexa\Contracts\ProductCatalog\Local\Attribute\ValueFormMapperInterface`. In this example, you can use the Symfony's built-in `PercentType` class (line 40). ```php getAttributeDefinition(); $options = [ 'disabled' => $context['translation_mode'] ?? false, 'label' => $definition->getName(), 'block_prefix' => 'percentage_attribute_value', 'required' => $assignment->isRequired(), 'constraints' => [ new AttributeValue([ 'definition' => $definition, ]), ], ]; if ($assignment->isRequired()) { $options['constraints'][] = new Assert\NotBlank(); } $builder->add($name, PercentType::class, $options); } } ``` The `options` array contains additional options for the form, including options resulting from the selected form type. Register the form mapper as a service and tag it with `ibexa.product_catalog.attribute.form_mapper.value`: ```yaml App\Attribute\Percent\Form\PercentValueFormMapper: tags: - name: ibexa.product_catalog.attribute.form_mapper.value type: percent ``` ## Create value formatter A value formatter prepares the attribute value for rendering in the proper format. In this example, you can use the `NumberFormatter` to ensure the number is rendered in the percentage form (line 22). ```php getValue(); if ($value === null) { return null; } $formatter = $parameters['formatter'] ?? null; if ($formatter === null) { $formatter = new NumberFormatter('', NumberFormatter::PERCENT); } return $formatter->format($value); } } ``` Register the value formatter as a service and tag it with `ibexa.product_catalog.attribute.formatter.value`: ```yaml App\Attribute\Percent\PercentValueFormatter: tags: - name: ibexa.product_catalog.attribute.formatter.value type: percent ``` ## Add attribute options You can also add options specific for the attribute type that the user selects when creating an attribute. In this example, you can set the minimum and maximum allowed percentage. ### Options type First, create `PercentAttributeOptionsType` that defines two options, `min` and `max`. Both those options need to be of `PercentType`. ```php add('min', PercentType::class, [ 'disabled' => $options['translation_mode'], 'label' => 'Minimum Value', 'required' => false, ]); $builder->add('max', PercentType::class, [ 'disabled' => $options['translation_mode'], 'label' => 'Maximum Value', 'required' => false, ]); } public function configureOptions(OptionsResolver $resolver): void { $resolver->setDefaults([ 'translation_mode' => false, ]); $resolver->setAllowedTypes('translation_mode', 'bool'); } } ``` ### Options form mapper Next, create a `PercentOptionsFormMapper` that maps the information that the user inputs in the form into attribute definition. ```php add($name, PercentAttributeOptionsType::class, [ 'constraints' => [ new AttributeDefinitionOptions(['type' => $context['type']]), ], 'translation_mode' => $context['translation_mode'], ]); } } ``` Register the options form mapper as a service and tag it with `ibexa.product_catalog.attribute.form_mapper.options`: ```yaml app.product_catalog.attribute.percent.form_mapper.options: class: App\Attribute\Percent\PercentOptionsFormMapper tags: - name: ibexa.product_catalog.attribute.form_mapper.options type: percent ``` ### Options validator Create a `PercentOptionsValidator` that implements `Ibexa\Contracts\ProductCatalog\Local\Attribute\OptionsValidatorInterface`. It validates the options that the user sets while creating the attribute definition. In this example, the validator verifies whether the minimum percentage is lower than the maximum. ```php get('min'); $max = $options->get('max'); if ($min !== null && $max !== null && $min > $max) { return [ new OptionsValidatorError('[max]', 'Maximum value should be greater than minimum value'), ]; } return []; } } ``` Register the options validator as a service and tag it with `ibexa.product_catalog.attribute.validator.options`: ```yaml app.product_catalog.attribute.options_validator.percent: class: App\Attribute\Percent\PercentOptionsValidator tags: - name: ibexa.product_catalog.attribute.validator.options type: percent ``` ### Value validator Finally, make sure the data provided by the user is validated. To do that, create `PercentValueValidator` that checks the values against `min` and `max` and dispatches an error when needed. ```php getOptions(); $min = $options->get('min'); if ($min !== null && $value < $min) { $errors[] = new ValueValidationError(null, 'Percentage should be greater or equal to %min%', [ '%min%' => $min, ]); } $max = $options->get('max'); if ($max !== null && $value > $max) { $errors[] = new ValueValidationError(null, 'Percentage should be lesser or equal to %max%', [ '%max%' => $max, ]); } return $errors; } } ``` Register the validator as a service and tag it with `ibexa.product_catalog.attribute.validator.value`: ```yaml app.product_catalog.attribute.value_validator.percent: class: App\Attribute\Percent\PercentValueValidator tags: - name: ibexa.product_catalog.attribute.validator.value type: percent ``` ## Storage To ensure that values of the new attributes are stored correctly, you need to provide a storage converter and storage definition services. ### Database schema design The values are going to be stored within a table named `app_product_specification_attribute_percent`, in a column named `value`. **MySQL** ```sql CREATE TABLE app_product_specification_attribute_percent ( id INT NOT NULL, value DOUBLE PRECISION DEFAULT NULL, INDEX app_product_specification_attribute_percent_value_idx (value), PRIMARY KEY (id) ) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_520_ci` ENGINE = InnoDB; ``` **PostgreSQL** ```sql CREATE TABLE app_product_specification_attribute_percent (id INT NOT NULL, value DOUBLE PRECISION DEFAULT NULL, PRIMARY KEY(id)); CREATE INDEX app_product_specification_attribute_percent_value_idx ON app_product_specification_attribute_percent (value); ALTER TABLE app_product_specification_attribute_percent ADD CONSTRAINT app_product_specification_attribute_percent_fk FOREIGN KEY (id) REFERENCES ibexa_product_specification_attribute (id) ON UPDATE CASCADE ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE; ``` ### Storage converter Start by creating a `PercentStorageConverter` class, which implements `Ibexa\Contracts\ProductCatalog\Local\Attribute\StorageConverterInterface`. This converter is responsible for converting database results into an attribute type instance: ```php $value, ]; } } ``` Register the converter as a service and tag it with `ibexa.product_catalog.attribute.storage_converter`: ```yaml App\Attribute\Percent\Storage\PercentStorageConverter: tags: - { name: 'ibexa.product_catalog.attribute.storage_converter', type: 'percent' } ``` ### Storage definition You can either create a new storage definition or use an existing one. To create a new storage definition, prepare a `PercentStorageDefinition` class, which implements `Ibexa\Contracts\ProductCatalog\Local\Attribute\StorageDefinitionInterface`. ```php Types::FLOAT, ]; } public function getTableName(): string { return 'app_product_specification_attribute_percent'; } } ``` Register the storage definition as a service and tag it with `ibexa.product_catalog.attribute.storage_definition`: ```yaml App\Attribute\Percent\Storage\PercentStorageDefinition: tags: - { name: 'ibexa.product_catalog.attribute.storage_definition', type: 'percent' } ``` If you prefer to use an existing storage definition, you need to create a Storage Definition Tag CompilerPass `src/DependencyInjection/AddFloatStorageDefinitionTag.php`: ```php getDefinition(StorageDefinition::class) ->addTag('ibexa.product_catalog.attribute.storage_definition', ['type' => 'percent']); } } ``` Add the CompilerPass to the container. Do it in a `src/Kernel.php` file or in your Bundle class: ```php addCompilerPass(new AddFloatStorageDefinitionTag()); } } ``` ## Use new attribute type In the back office you can now add a new Percent attribute to your product type and create a product with it. ![Creating a product with a custom Percent attribute](https://doc.ibexa.co/en/saas/product_catalog/img/catalog_custom_attribute_type.png) # Create custom availability strategy > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). Implement custom availability strategies to handle different business scenarios, for example pre-orders or per-region availability. The product catalog uses an availability strategy to calculate [computed availability](https://doc.ibexa.co/en/saas/product_catalog/products/#availability-and-computed-availability) for a product. Computed availability decides whether the customers can order the product. The default availability strategy is based on the product availability and stock amount. You can replace this logic with a custom strategy to handle specific business scenarios, for example preorders, minimum order quantities, or per-region availability. The following example implements an availability strategy which allows buying products when they're set as available, without taking their stock into account. You could use it for [virtual products](https://doc.ibexa.co/en/saas/product_catalog/products/#product-types) or in preorder scenarios. ## Create custom availability context Use an availability context to pass the parameters needed by the strategy to evaluate computed availability. To do it, create a class that implements the [`AvailabilityContextInterface`](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-ProductCatalog-Values-Availability-AvailabilityContextInterface.html) interface: ```php handler->find($product->getCode()); $rawAvailableFlag = $productAvailability->isAvailable(); $stock = $productAvailability->getStock(); $isInfinite = $productAvailability->isInfinite(); $computedAvailable = $this->calculateAvailability( $rawAvailableFlag, $stock, $isInfinite, ); return new Availability( $product, $rawAvailableFlag, $computedAvailable, $isInfinite, $stock, ); } private function calculateAvailability( bool $rawAvailable, ?int $stock, bool $isInfinite ): bool { if ($rawAvailable === false) { return false; } if ($isInfinite) { return true; } if ($stock === null) { return true; } return $stock >= 0; } } ``` The strategy has two methods: - `accept()` decides if the strategy can handle the provided availability context - `getProductAvailability()` returns an [`AvailabilityInterface`](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-ProductCatalog-Values-Availability-AvailabilityInterface.html) object When constructing the `AvailabilityInterface` object, provide the stock amount, the availability flag, and the result of your custom availability logic. ## Register strategy as a service If you're not using [autowiring](https://symfony.com/doc/7.4/service_container/autowiring.html), tag the strategy service with `ibexa.product_catalog.availability.strategy`: ```yaml services: App\ProductCatalog\Availability\ProductAvailabilityPurchasableWithoutStockStrategy: tags: - { name: ibexa.product_catalog.availability.strategy } ``` ## Use custom context To evaluate product availability using a custom strategy, pass the custom context as the second argument to [`ProductAvailabilityServiceInterface::getAvailability()`](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-ProductCatalog-ProductAvailabilityServiceInterface.html): ```php ``` # Create custom catalog filter > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). Fine-tune catalogs by adding a custom catalog filter for selecting products from the Product catalog. Catalog filters let you narrow down the products from the Product catalog that are available in the given [catalog](https://doc.ibexa.co/en/saas/product_catalog/catalogs/index.md). Besides the built-in catalog filters, you can also create custom ones. The following example shows how to create a filter that selects products with the entered name. ## Create filter class To create a custom catalog filter, first you need to create a filter class in `App\CatalogFilter\ProductNameFilter`. ```php add( $filterDefinition->getIdentifier(), TagifyType::class, [ 'label' => 'Product name', 'block_prefix' => 'catalog_criteria_product_name', 'translation_domain' => 'product_catalog', ] ); $builder->get($filterDefinition->getIdentifier()) ->addModelTransformer( new DataTransformer\ProductNameCriterionTransformer() ); } public function supports(FilterDefinitionInterface $filterDefinition): bool { return $filterDefinition instanceof ProductNameFilter; } } ``` The filter can use the built-in `ProductName` Criterion, but you still need a data transformer for the data entered when editing the catalog (line 35). Before you add a data transformer, register the required services. ## Register services Register the filter and its mapper as services. Tag the filter with `ibexa.product_catalog.catalog_filter` and the form mapper with `ibexa.product_catalog.catalog_filter.form_mapper`: ```yaml services: App\CatalogFilter\ProductNameFilter: tags: - name: ibexa.product_catalog.catalog_filter alias: product_name App\CatalogFilter\ProductNameFilterFormMapper: tags: - name: ibexa.product_catalog.catalog_filter.form_mapper ``` ## Create data transformer Now, create `ProductNameCriterionTransformer` in `src/CatalogFilter/DataTransformer`: ```php */ final class ProductNameCriterionTransformer implements DataTransformerInterface { public function transform($value): ?string { if (null === $value) { return null; } if (!$value instanceof ProductName) { throw new TransformationFailedException('Expected a ' . ProductName::class . ' object.'); } return $value->getName(); } public function reverseTransform($value): ?ProductName { if ($value === null) { return null; } if (!is_string($value)) { throw new TransformationFailedException('Invalid data, expected a string value'); } return new ProductName($value); } } ``` ## Provide templates Now, provide the templates for the catalog editing view in the back office. You need two templates: one for the filter form, and one for the filter badge in the product list. First, add a `form_field_override.html.twig` template to `templates/themes/admin/product_catalog`: ```html+twig {% extends '@ibexadesign/product_catalog/form_fields.html.twig' %} {%- block catalog_criteria_product_name_row -%} {{- block('catalog_taggify_panel') -}} {%- endblock -%} ``` Here, you use the same built-in template that is used for example for the product code filter. It's placed in a template block corresponding to your custom filter, `catalog_criteria_product_name_values`. To ensure the template is used as a back office form theme, add the following configuration: ```yaml twig: form_themes: - '@ibexadesign/product_catalog/form_field_override.html.twig' ``` Next, add a template that handles the display of the filter badge on the list of the currently filtered products. Add `catalog_filters_blocks.html.twig` to `templates/themes/admin/product_catalog`: ```html+twig {% block catalog_criteria_product_name_values %} {% include '@ibexadesign/product_catalog/catalog/edit/list_filter_taggify.html.twig' with { criteria } %} {% endblock %} ``` To ensure this template is used to render the catalog filter form, add the following configuration: ```yaml ibexa: system: default: product_catalog: catalogs: filter_preview_templates: - { template: "@ibexadesign/product_catalog/catalog_filters_blocks.html.twig", priority: 10 } ``` ## Check results Finally, you can check the results. Go to **Product catalog** -> **Catalogs** and create a new catalog. From the filter list, select **Product name**, type the name of an existing product and click **Save**. ![Custom Product Name catalog filter](https://doc.ibexa.co/en/saas/product_catalog/img/custom_catalog_filter.png) # Create custom name schema strategy > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). Create custom name schema strategy to generate URL aliases based on attribute values. You can create custom name schema strategy to generate URL aliases based on attribute values. Make sure the attributes are configured correctly. Each attribute that you want to include in the URL alias must have a name schema strategy. ## Create converting class Start by creating a `PercentNameSchemaStrategy` class, which implements `\Ibexa\Contracts\ProductCatalog\NameSchema\NameSchemaStrategyInterface`. This class is responsible for converting attribute values into a string of URL parameters: ```php getType()->getIdentifier() === 'percent'; } } ``` ## Register strategy Next, you need to register the strategy in the dependency injection container: ```yaml services: _defaults: public: false autowire: true autoconfigure: true App\Attribute\Percent\PercentNameSchemaStrategy: tags: - { name: ibexa.product_catalog.naming_schema_strategy } ``` This ensures that the custom name schema strategy is available for use in generating URL aliases based on attribute values. # Create custom product code generator strategy > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). A custom product code generator enables you to control how product codes are created. Product code generator strategies control what product variant codes are generated. Besides the [built-in](https://doc.ibexa.co/en/saas/product_catalog/product_catalog_configuration/#code-generation-strategy) strategies, you can create your own ones. A code generator strategy must implement `Ibexa\Contracts\ProductCatalog\Local\CodeGenerator\CodeGeneratorInterface`. The following example shows how to add a generator strategy that creates code, based on the product base code and an incremental index number. First, create the generator strategy class: ```php hasBaseProduct()) { throw new InvalidArgumentException('$context', 'missing base product'); } if (!$context->hasIndex()) { throw new InvalidArgumentException('$context', 'missing index'); } return $context->getBaseProduct()->getCode() . 'v' . $context->getIndex(); } } ``` This generator uses the provided context to get product information (in this case the code of the base product) and the incremental number. Then, register the strategy generator as a service and tag it with `ibexa.product_catalog.code_generator`: ```yaml services: App\CodeGenerator\Strategy\CustomIncrementalCodeGenerator: tags: - { name: 'ibexa.product_catalog.code_generator', type: 'custom_incremental' } ``` Use the defined `type` in [catalog configuration](https://doc.ibexa.co/en/saas/product_catalog/product_catalog_configuration/#code-generation-strategy) to apply codes generated by this strategy to new product variants. # Customize product embed templates > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). Customize the templates used to render products embedded in RichText fields. When a product is [embedded in a RichText field](https://doc.ibexa.co/en/saas/product_catalog/products/#embed-products-in-content), it is rendered by using a Twig template. You can override the default templates to customize the appearance of embedded products. ## Embed types Six embed types exist in the system, each with its own template: | Embed type | Description | | -------------------------- | ----------------------------------------------------------------------- | | `product` | Block-level embed when the product is found and the user has access. | | `product_inline` | Inline embed when the product is found and the user has access. | | `product_denied` | Block-level embed when the user has no access to view the product data. | | `product_inline_denied` | Inline embed when the user has no access to view the product data. | | `product_not_found` | Block-level embed when the product code cannot be found. | | `product_inline_not_found` | Inline embed when the product code cannot be found. | ## Template variables The following variables are available in the embed templates: | Variable | Available in | Description | | ------------- | ------------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `product` | `product`, `product_inline` | A [`ProductInterface`](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-ProductCatalog-Values-ProductInterface.html) object. | | `productCode` | `product_denied`, `product_inline_denied`, `product_not_found`, `product_inline_not_found` | The product code string, used to identify the product that could not be loaded. | | `embedParams` | All block types | Optional parameters set by the online editor, for example `align` or `class` properties | ## Override template The default templates are located in `vendor/ibexa/product-catalog/src/bundle/Resources/views/themes/standard/product_catalog/richtext/embed/`. To override a template, create a file with the same name in your [theme directory](https://doc.ibexa.co/en/saas/templating/design_engine/design_engine/index.md). For example, to override the block embed template for the `standard` theme, create a file in `templates/themes/standard/product_catalog/richtext/embed/product.html.twig` A minimal product embed template looks as follows: ```html+twig
    {{ product.name }}
    ``` And a minimal inline embed template (`product_inline.html.twig`): ```html+twig {{ product.name }} ``` ## Configure template paths In addition to overriding the templates with the [design engine](https://doc.ibexa.co/en/saas/templating/design_engine/design_engine/index.md), you can explicitly set the template path for any embed type in your [SiteAccess configuration](https://doc.ibexa.co/en/saas/multisite/multisite_configuration/index.md): ```yaml ibexa: system: : fieldtypes: ibexa_richtext: embed: product: template: '@ibexadesign/product_catalog/richtext/embed/product.html.twig' product_inline: template: '@ibexadesign/product_catalog/richtext/embed/product_inline.html.twig' product_denied: template: '@ibexadesign/product_catalog/richtext/embed/product_denied.html.twig' product_inline_denied: template: '@ibexadesign/product_catalog/richtext/embed/product_inline_denied.html.twig' product_not_found: template: '@ibexadesign/product_catalog/richtext/embed/product_not_found.html.twig' product_inline_not_found: template: '@ibexadesign/product_catalog/richtext/embed/product_inline_not_found.html.twig' ``` Replace `` with the name of your SiteAccess or SiteAccess group (for example, `default`). # Add Remote PIM support > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). Install and configure the Remote PIM example package. Cohesivo provides flexible product catalog infrastructure that works with external Product Information Management (PIM) systems. For advanced product data management without custom development, you can use the readily available [Quable integration](https://doc.ibexa.co/en/saas/product_catalog/quable/quable/index.md) with Cohesivo. To implement [Remote PIM support](https://doc.ibexa.co/en/saas/product_catalog/product_catalog_guide/#remote-pim-support) for a custom integration, you can build upon a foundation provided by Ibexa. While doing so, you must implement services that process data coming from the remote PIM. Before you create your own solution, you can [install an example package](#install-remote-pim-example-package) and modify it to connect to your external data source. ## Implement services To connect to your remote PIM, provide your implementation of the following services that process product data: - AssetService, used to get assets assigned to a product. - AttributeDefinitionService, used to get information about product attributes. - AttributeGroupService, used to get information about product attribute groups. - ProductService, used to get product information. - ProductTypeService, used to work with product types. ## Switch to the new product catalog engine To inform the application that the product catalog engine has been replaced by an external one, in `config/packages/ibexa_product_catalog.yaml`, set the new product catalog engine, for example: ```yaml ibexa_product_catalog: engines: : type: options: root_location_remote_id: ibexa_product_catalog_root ``` Then configure the application to use the engine defined above as the default product data repository: ```yaml ibexa: repositories: : # ... product_catalog: engine: ``` > **Note: Enabling the remote PIM support** > > By default, the `ibexa.repositories..product_catalog.engine.type` key is set to `local`, which informs Cohesivo that the built-in product catalog capabilities are used. By changing this setting and the `ibexa.repositories..product_catalog.engine` setting from `default` to your custom value, you inform Cohesivo that you're using a remote PIM. ## Install Remote PIM example package The example implementation provides services that take over the role of services provided by the product catalog package. You can modify them to suit your needs. Install the `ibexa/example-in-memory-product-catalog` package: ```bash composer config repositories.remote-pim vcs https://github.com/ibexa/example-in-memory-product-catalog composer require ibexa/example-in-memory-product-catalog: ``` # Customer management # Customer Portal > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). Customer Portal allows your business clients to create and manage their company accounts. Editions: Experience A Customer Portal serves as a central entry point to your services and products. It helps you provide a unique user experience with a single point of access to any relevant self-service options for your products and services. Cohesivo Customer Portal and customer management that ships with it let you create and handle business accounts and communicate with your partners in a personalized space. With this feature, your customers can self-register, edit their organization information, invite and view members, check their order history, and more. - [Customer Portal product guide](https://ez-systems-developer-documentation--3381.com.readthedocs.build/en/3381/customer_management/customer_portal_guide/): Check all the capabilities and advantages that the Customer Portal offers to the clients by reading the Customer Portal product guide. - [Customer Portal configuration](https://ez-systems-developer-documentation--3381.com.readthedocs.build/en/3381/customer_management/cp_configuration/): Configure Customer Portal to fit the needs of your business. - [Customer Portal applications](https://ez-systems-developer-documentation--3381.com.readthedocs.build/en/3381/customer_management/cp_applications/): Customization of an approval process for new companies applications. - [Inviting users](https://ez-systems-developer-documentation--3381.com.readthedocs.build/en/3381/users/invitations/): Manage user invitations to create an account in the frontend or the back office. - [Create Customer Portal](https://ez-systems-developer-documentation--3381.com.readthedocs.build/en/3381/customer_management/cp_page_builder/): Create unique Customer Portals for your clients with Page Builder. - [Create user registration form](https://ez-systems-developer-documentation--3381.com.readthedocs.build/en/3381/customer_management/create_user_registration_form/): Customize the registration form for new users in your site front end. # Customer Portal product guide > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). Check all the capabilities and advantages that the Customer Portal offers to the clients by reading the Customer Portal product guide. Editions: Experience ## What is Customer Portal A Customer Portal serves as a central entry point to your services and products. It helps you provide a unique user experience with a single point of access to any relevant self-service options for your products and services. Cohesivo Customer Portal and customer management that ships with it let you create and handle business accounts and communicate with your partners in a personalized space. With this feature, your customers can self-register, edit their organization information, invite and view members, check their order history, and more. ## Availability Customer Portal is available in Ibexa Experience. It's also compatible with Product catalog and Ibexa Connect. ## How does Customer Portal work? Customer Portal is a component based on content types. This means that Cohesivo provides containers, user management, content management, so you can focus on business logic and general outlook of the portal for your B2B clients. ### Customer Portal The Customer Portal allows company members to log in and manage their profiles and order history. With user differentiation, company buyers can only purchase products while company admins can invite and manage members and change company information, such as billing addresses. ![Customer Portal dashboard](https://doc.ibexa.co/en/saas/customer_management/img/cp_dashboard_customer_portal.png) ### Editable in Page Builder Custom Customer Portal can be created and edited in Page Builder to meet the needs of each business type, company, or market they operate on. To create a new Customer Portal, go to **Content** and, from the menu, select **Content structure**. There, navigate to the root container for your Customer Portals and select **Customer Portal Page**. In the Page Builder creation box, you see the Customer Portal layout where you can add a dedicated Customer Portal block, Sales Representative, or choose from a selection of blocks available to your Cohesivo version. If the built-in page blocks aren't sufficient to fulfill your needs, you can add your own. ![Editable in Page Builder](https://doc.ibexa.co/en/saas/customer_management/img/cp_edit_in_page_builder.png) You can allow company members to see multiple versions of Customer Portal on a single page by adding them under one Customer Portal container and combining SiteAccess matchers. This setup is recommended for global markets or company-specific portals, where each portal is designed specifically for its customers and their needs. ![Multiple portals](https://doc.ibexa.co/en/saas/customer_management/img/cp_2_page_view.png) ### Company management The main company management takes place in the back office where each company has its own profile where sales representative can find: - summary with basic information and order history - company profile with billing information and contact person - list of members and pending invitations - address book with multiple shipping addresses ![Companies section in back office](https://doc.ibexa.co/en/saas/customer_management/img/cp_back_office.png) From there, they can activate and deactivate the company, edit its information, invite members, manage their roles, and edit their basic information. In the roles section, you can define policies for each user group, for example, a Company buyer. You can also set up policies for every user who has a business account by editing a Corporate Access role. ### Members Company members aren't standard users. They belong to a separate category called Corporate Accounts. This category is located in **Admin** -> **Corporate** -> **Corporate Accounts**. There, you can find a list of companies and their members. This feature comes with a set of new roles: - Member — users who are members of a company - Corporate Access — users who can log into Customer Portal - Company Admin — users who can edit company's details - Company Buyer — users who can buy in company's name All roles and policies associated with them can be fully customized to fit your business needs. ### Invitations Members can be invited to the organization from: - the back office: go to **Customers** -> **Companies** -> **Select your company** -> **Invitations** -> **Invite member** - the Customer Portal: go to your company admin profile, select **Members** -> **Invite members** Then, in a pop-up fill out email addresses one by one, or use drag and drop to upload a file with a list of emails. You also have to assign a role to each new member from a drop-down list. Click **Send** to send out invitations. ![Invitations](https://doc.ibexa.co/en/saas/customer_management/img/cp_invitations.png) Invited users receive an email message with a registration link. With it, they can register and create their account in the Customer Portal. ![Create account](https://doc.ibexa.co/en/saas/customer_management/img/cp_create_account.png) ### Company self-registration Self-registration allows business customers to take charge and apply for a business account by themselves. Applications go through the approval process in the back office where they can be accepted, rejected or put on hold. If they're accepted, the business partner receives an invitation link to the Customer Portal, where they can set up their team and manage their account. To apply for a business account, a company needs to provide their basic information, contact information and billing address in an application. ![Company self-registration](https://doc.ibexa.co/en/saas/customer_management/img/cp_registration.png) The approval process is customizable. You can decide which user has approval rights by granting them `Company Application/Workflow` policy, you can also decide between which states the user may move applications: - on hold - accept - reject If built-in statuses aren't sufficient, you can add custom ones. You can also edit or add reasons for not accepting the company application. Finally, you can customize the registration site itself. ### REST API Customer Portal comes with [REST API](https://doc.ibexa.co/en/saas/api/rest_api/rest_api_reference/rest_api_reference.html#tag/Corporate-Account) for interacting with corporate accounts from the context of the Ibexa Connect app. ## Capabilities ### Company management Sales representatives can manage details for companies they're associated with, such as contact persons, billing addresses, and more by accessing back office. Company admins are also able to manage the company's details in the Customer Portal interface. By giving users power to manage their own accounts, you reduce the need for administrative interventions. ### Self registration Self-registration allows your customers to take control of their business accounts. This not only improves customer satisfaction but also reduces the administrative burden on your team. With the ability to integrate with Ibexa Connect, you're able to fully automate the process. ### Address book Use of an address book allows you to add many shipping addresses to one company for clients with multiple locations. ### Custom prices You can offer special prices and additional discounts dedicated for Customer groups containing company members with verified accounts. ### Available in segments Corporate accounts are available in segments, which means you can assign companies to different recommendation groups based on gathered data, and deliver recommendations. It allows you to make use of customer targeting of the segments and create personalized experience for each company. ## Benefits ### General overview The overall benefit of customer portals is the help they provide to retain customers and increase loyalty, while freeing up customer service employees time for higher-level work. They can achieve that by providing customers with up-to-date information about their orders and deliveries, personalize shopping experience, offer special deals available only to B2B partners, and do that in one, accessible space. Currently, Customer Portals are a standard in global sites such as Amazon. They're the level of quality that customers expect, and all businesses strive for. ### Simplified shopping process Business account helps streamline the B2B shopping process with all the paperwork, payment, and other administrative tasks converted into a few steps with prefilled forms, billing addresses, shipping addresses, and more. Making your site a go-to place for company orders. ### Better customer experience In the era of internet, customers expect quick, accessible and excellent quality service, and user experience from every business they associate with. Customer portals offer a seamless self-service experience by providing complete 24/7 access to relevant, up-to-date information and customer support. ### Client encouragement Price strategies are a great way to build and maintain strong relationships with your trading partners. With special prices available to B2B clients, you can offer the best deals in highly competitive markets. Those discounts may be a great encouragement to convince big buyers to choose your business over other options. Competitive prices impact not only the size of the customer base, they affect every customer’s purchasing strategy, including the diversity, frequency, and volume of their orders. ### Cost benefits Customer portals help you to automate tasks that otherwise would be done by your employees manually, such as customer services, checking shipment status. An additional benefit of customer portals is their availability 24/7. Thus, reducing the need to allocate resources to extend working hours or hire more employees. ### Localization and recommendations The use of Page Builder in the Customer Portal creation process enables you to create unique experiences for each business customer based on their location, business type, company, or market they operate on. # Customer Portal configuration > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). Configure Customer Portal to fit the needs of your business. Editions: Experience You can overwrite the default configuration of the Customer Portal to fit its capabilities to the unique needs of your business. ## `corporate` SiteAccess The predefined `corporate` SiteAccess in `corporate_group` (configured in `config/packages/ibexa.yaml`) serves the Customer Portal. If you need a multisite setup with multiple Customer Portals, add any additional SiteAccesses to `corporate_group`. ## Customer identifier `ibexa_default_settings.yaml` contains a setting that indicates what content types should be treated like Users in terms of, for example, usage in `UserService`: ```yaml ibexa: system: default: user_content_type_identifier: ['user', 'customer'] ``` ## Roles and policies You can add custom roles to your installation by listing them under the `ibexa.site_access.config.default.corporate_accounts.roles` [configuration](https://doc.ibexa.co/en/saas/administration/configuration/configuration/#configuration-files). This key overwrites the default list set in `vendor/ibexa/corporate-account/src/bundle/Resources/config/default_settings.yaml` (the following example redeclares them for clarity): ```yaml parameters: ibexa.site_access.config.default.corporate_accounts.roles: admin: Company Admin buyer: Company Buyer custom_role: Company Assistant ``` You can do it per SiteAccess or SiteAccess group by using [SiteAccess-aware configuration](https://doc.ibexa.co/en/saas/multisite/siteaccess/siteaccess_aware_configuration/index.md). ## Content type names You can change names of default content types by assigning what content types should be used to describe `Company` and `Member` in the back office. Proceed only if you already have a `Company` content type in your system, and you don't want to change its identifier. Configuration for content type names is placed under the `ibexa_corporate_account` key, like shown in `Ibexa\Bundle\CorporateAccount\DependencyInjection\Configuration`. To change content type names, adjust corporate account configuration in the following way: ```yaml ibexa_corporate_account: content_type_mappings: company: your_ct_identifier ``` > **Caution: Migration** > > If you decide to change deafult names of content types, during migration you have to adjust files accordingly. ## Registration You can define what fields are required in the Customer Portal registration form. To do so, [create and configure user registration form](https://doc.ibexa.co/en/saas/customer_management/create_user_registration_form/index.md). ## Address With the Address field type, you can customize address fields and configure them per country. To learn more, see [Address field type documentation](https://doc.ibexa.co/en/saas/content_management/field_types/field_type_reference/addressfield/index.md). ## Templates You can also define new templates for, among others: invitation email, reset password message and the information screens after any of the user's actions. ```yaml ibexa: system: site_group: content_view: full: confirmation_page: template: "@@ibexadesign/customer_portal/account/forgot_password/confirmation_page.html.twig" match: Identifier\ContentType: confirmation_page ``` ## Order management Reviewing pending and past orders in Customer Portal requires that you configure all currencies that any of the customers may use under the `ibexa.system..product_catalog.currencies` key. The first currency from the list is then used for filtering the orders list and calculating the **Average order** and **Total amount** values. For more information, see [Enable purchasing products](https://doc.ibexa.co/en/saas/product_catalog/enable_purchasing_products/index.md). # Create Customer Portal > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). Create unique Customer Portals for your clients with Page Builder. Editions: Experience On this page, you can learn how to configure the Customer Portal feature to be editable with Page Builder. If you already configured Customer Portal, you can learn how to build it with a Page Builder in [User Documentation](https://doc.ibexa.co/projects/userguide/en/6.0/customer_management/build_customer_portal/). First, you need to decide if you want to create and configure [one portal](#create-and-configure-one-portal) or [multiple portals](#create-and-configure-multiple-portals) setup. ## Create and configure one portal This setup is recommended for use cases with one Customer Portal for all markets. If you plan to expand your portal portfolio in the future, see [multiple portal configuration](#create-and-configure-multiple-portals). ### Configure Page Builder access to Customer Portal First, create a Customer Portal page, its location ID needs to be later specified in the configuration. To do it, go to **Content** -> **Content structure**, and select **Customer Portal Page**. For now, you only need to add a name and a description in the field view, you can find it in the upper toolbar on the left side. Next, click **Publish** to see the page in the content tree. ![Add name and description to Customer Portal](https://doc.ibexa.co/en/saas/customer_management/img/cp_name_description.png) To be able to see the Customer Portal site template in the Page Builder you need to add `custom_portal` SiteAccess to the configuration. First, under the `ibexa.siteaccess` [configuration key](https://doc.ibexa.co/en/saas/administration/configuration/configuration/#configuration-files) add `custom_portal` to the SiteAccess `list` and to `corporate_group`. Next, add configuration for `corporate_group` and `custom_portal` under `ibexa.system`. Remember to specify `location_id` of your Customer Portal, you can find it under the **Technical details** tab of your new page. ```yaml ibexa: siteaccess: list: - import - site - admin - corporate - custom_portal groups: site_group: [import, site] storefront_group: [site] corporate_group: [corporate, custom_portal] system: corporate_group: languages: [eng-GB] custom_portal: languages: [ eng-GB ] content: tree_root: location_id: 12345 # location_id_of_customer_portal excluded_uri_prefixes: [ /media/, /images/ ] ``` Next, under the `ibexa.system.admin.page_builder` [configuration key](https://doc.ibexa.co/en/saas/administration/configuration/configuration/#configuration-files), add `custom_portal` to [the SiteAccess list available to Page Builder](https://doc.ibexa.co/en/saas/multisite/multisite_configuration/#siteaccesses-and-page-builder): ```yaml ibexa: system: admin: page_builder: siteaccess_list: - site - corporate - custom_portal ``` Now, you can go to your Customer Portal landing page and edit it in Page Builder. ![Edit Customer Portal in Page Builder](https://doc.ibexa.co/en/saas/customer_management/img/cp_edit_in_page_builder.png) ### Grant permissions to customers You need to grant the following permissions to company members, so they can view custom Customer Portal: - `user/login` to `custom_portal` SiteAccess - `content/read` to the Customer Portal ![Single Customer Portal permissions](https://doc.ibexa.co/en/saas/customer_management/img/single_cp_permissions.png) If members of the company don't have sufficient permissions for any Customer Portal, they're redirected to the default Customer Portal view. > **Note: Note** > > Customer Portal is only available to users that are members of the company. Even if a user has all the sufficient permissions but isn't a member of a company, this user cannot see the Customer Portal. ## Create and configure multiple portals This setup is recommended for global markets or company specific portals, where each portal is design specifically for its users and their needs. ### Customer Portal container First, you need to create a root folder for Customer Portals, its location ID needs to be later specified in the configuration as [a tree root](https://doc.ibexa.co/en/saas/multisite/multisite_configuration/#location-tree). To do it, go to **Content** -> **Content structure**, and select **Create content**. There you can see two possibilities **Customer Portal** and **Customer Portal Page**. ![Create content tab](https://doc.ibexa.co/en/saas/customer_management/img/cp_portal_vs_page.png) The first one is a separate content type used as a container for your Customer Portal pages. Customer Portals containers should be used to sort Customer Portal pages and any other content types used by them, such as articles, inside the root folder. It's recommended that you use them instead of folders to divide and store your portals. Select **Customer Portal**, define its name and publish. ![Customer Portals folder](https://doc.ibexa.co/en/saas/customer_management/img/cp_folder_for_portals.png) ### Configure Page Builder access to Customer Portal To be able to see Customer Portal site template in the Page Builder you need to add `custom_portal` SiteAccess to the configuration. First, under the `ibexa.siteaccess` [configuration key](https://doc.ibexa.co/en/saas/administration/configuration/configuration/#configuration-files), add `custom_portal` to the SiteAccess `list` and to `corporate_group`. Next, add configuration for `corporate_group` and `custom_portal` under `ibexa.system`. Remember to specify `location_id` of the root folder for Customer Portals, you can find it under the **Technical details** tab. ```yaml ibexa: siteaccess: list: - import - site - admin - corporate - custom_portal groups: site_group: [import, site] storefront_group: [site] corporate_group: [corporate, custom_portal] system: corporate_group: languages: [eng-GB] custom_portal: languages: [ eng-GB ] content: tree_root: location_id: 12345 # location_id_of_customer_portals_root_folder excluded_uri_prefixes: [ /media/, /images/ ] ``` Next, under the `ibexa.system.admin.page_builder` [configuration key](https://doc.ibexa.co/en/saas/administration/configuration/configuration/#configuration-files), add `custom_portal` to [the SiteAccess list available to Page Builder](https://doc.ibexa.co/en/saas/multisite/multisite_configuration/#siteaccesses-and-page-builder): ```yaml ibexa: system: admin: page_builder: siteaccess_list: - site - corporate - custom_portal ``` Now, you can go back to your Customer Portal's container. All landing pages that you create in it use Customer Portal template. ### Assign portal to Customer group You can assign multiple Customer Portal containers or Pages to a specific Customer group. First, you need to grant the following permissions to company members from the Customer group: - `user/login` to `custom_portal` SiteAccess - `content/read` to selected Customer Portals ![Customer Portal permissions](https://doc.ibexa.co/en/saas/customer_management/img/cp_permissions.png) If members of the Customer group don't have sufficient permissions for any Customer Portal assigned to them, they're redirected to the default Customer Portal view. > **Note: Note** > > Customer Portal is only available to users that are members of the company. Even if user has all the sufficient permissions but isn't a member of a company, this user cannot see the Customer Portal. #### Build-in portal mapping Now, you need to assign your custom portals to Customer groups. Add portal mapping configuration in `config/services.yaml`: ```yaml parameters: ibexa.corporate_account.customer_portal.customer_group_to_portal_map: eu: - 6bd4c938f9b3f668057c7e20987fac6c - 7bf85988a77ee859f2466av2b42bd909 us: - 6ce85480aeaeed59f7431a12b46bc869 ``` There, you can specify which Customer Portals should be available to which Customer group by adding: - Customer group identifier. You can find it in the **Summary** section of the Company. - Location remote ID of Customer Portal container or Customer Portal page. You can find it in the **Details** section. Portals are displayed to the Customer group in order specified in the configuration based on company member's permissions. #### Custom portal mapping You can specify your own custom logic for redirecting members to a specific Customer Portal. To do so, implement `\Ibexa\Contracts\CorporateAccount\CustomerPortal\PickRule\CustomerPortalPickRule` and tag it with `ibexa.corporate_account.customer_portal.pick_rule`. ### Multiple portals on single page You can allow company members to see multiple versions of Customer Portal on a single page by [combining SiteAccess matchers](https://doc.ibexa.co/en/saas/multisite/siteaccess/siteaccess_matching/#custom-matchers) with `Compound\LogicalAnd`: ```yaml ibexa: siteaccess: match: Compound\LogicalAnd: custom_portal: matchers: Map\Port: eu: true Map\Host: example.com: true match: custom_portal Map\Host: admin.example.com: site_admin ``` ![Multiple portals in one view](https://doc.ibexa.co/en/saas/customer_management/img/cp_2_page_view.png) ## Change Customer Portal layout You can change Customer Portal layout by adding your custom template under `ibexa.system..page_layout`: ```yaml ibexa: system: custom_portal: languages: [ eng-GB ] page_layout: "@App/my_page_layout.html.twig" content: tree_root: location_id: 12345 #location_id_of_customer_portals_root_folder excluded_uri_prefixes: [ /media/, /images/ ] ``` To generate the Customer Portal menu you should use `customer_portal.menu.main` key: ```html+twig {% block side_column %}
    {% block left_sidebar %} {% set main_menu = knp_menu_get('customer_portal.menu.main', [], {}) %} {{ knp_menu_render(main_menu, { depth: 1, template: '@ibexadesign/customer_portal/menu.html.twig', currentClass: 'active', ancestorClass: 'active', }) }} {% endblock %}
    {% endblock %} ``` To learn more about creating a menu, see [Add navigation menu](https://doc.ibexa.co/en/saas/templating/layout/add_navigation_menu/index.md). # Customer Portal applications > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). Customization of an approval process for new companies applications. Editions: Experience New business customers can apply for a company account. Applications go through the approval process in the back office where they can be accepted, rejected or put on hold. If they're accepted, the business partner receives an invitation link to the Customer Portal, where they can set up their team and manage their account. For more information on company self-registration, see [user guide documentation](https://doc.ibexa.co/projects/userguide/en/6.0/customer_management/company_self_registration/). If provided options are too limited, you can customize an approval process by yourself. ## Roles and policies Any user can become application approver, as long as they have the `Company Application/Workflow` policy assigned to their role. There, you can define between which states the user may move applications. For example, the assistant can put new applications on hold, or reject them, and only the manager can accept them. ![Company Application policy](https://doc.ibexa.co/en/saas/customer_management/img/cp_company_application_policy.png) ## Customer Portal application configuration Below, you can find possible configurations for Customer Portal applications. ### Reasons for rejecting application To change or add reasons for not accepting Corporate Portal application go to `vendor/ibexa/corporate-account/src/bundle/Resources/config/default_settings.yaml`. ```yaml parameters: ibexa.site_access.config.default.corporate_accounts.reasons: reject: [Malicious intent / Spam] on_hold: [Verification in progress] ``` ### Timeout Registration form locks for 5 minutes after unsuccessful registration, if the user, for example, tried to use an email address that already exists in a Customer Portal clients database. To change that duration, go to `config/packages/ibexa.yaml`. ```yaml framework: rate_limiter: corporate_account_application: policy: 'fixed_window' limit: 1 interval: '5 minutes' lock_factory: 'lock.corporate_account_application.factory' ``` ## Customization of an approval process In this procedure, you add a new status to the approval process of business account application. ### Add new status First, under the `ibexa.system..corporate_accounts.application.states` add a `verify` status to the [configuration](https://doc.ibexa.co/en/saas/administration/configuration/configuration/#configuration-files): ```yaml ibexa: system: default: corporate_accounts: application: states: [ 'new', 'accept', 'on_hold', 'reject', 'verify' ] ``` ### Create new Form Type Next, create a new form type in `src/Form/VerifyType.php`. It's displayed in the application review stage. ```php add(self::FIELD_APPLICATION, HiddenType::class) ->add('new_field', TextType::class) ->add(self::FIELD_NOTES, TextareaType::class, [ 'required' => false, ]) ->add(self::FIELD_VERIFY, SubmitType::class); } } ``` Line 29 defines where the form should be displayed, line 21 adds **Note** field, and line 22 adds the **Verify** button. ### Create event subscriber to pass the form Add an event subscriber that passes a new form type to the frontend. Create `src/Corporate/EventSubscriber/ApplicationDetailsViewSubscriber.php` following the example below: ```php getApplication(); $view->addParameters([ 'verify_form' => $this->formFactory->create( VerifyType::class, [ 'application' => $application->getId(), ] )->createView(), ]); } protected function supports(View $view): bool { return $view instanceof ApplicationDetailsView; } } ``` In line 39, you can see the `verify_form` parameter that passes the `verify` form to the application review view. ### Add form template To be able to see the changes you need to add a new template `templates/themes/admin/corporate_account/application/details.html.twig`. ```html+twig {% extends "@IbexaCorporateAccount/themes/admin/corporate_account/application/details.html.twig" %} {% block content %} {{ form_start(verify_form, { action: path('ibexa.corporate_account.application.workflow.state', { state: 'verify', applicationId: application.id, }), method: 'POST'}) }} {{ form_row(verify_form.notes) }}
    {{ form_widget(verify_form.verify, { attr: { class: 'ibexa-btn ibexa-btn--primary ibexa-ca-application-workflow-extra-actions__btn', }}) }}
    {{ form_end(verify_form) }} {{ parent() }} {% endblock %} ``` It overrides the default view and adds a **Verify** button to the review view. To check the progress, go to **Members** -> **Applications**. Select one application from the list and inspect application review view for a new button. ![Verify button](https://doc.ibexa.co/en/saas/customer_management/img/cp_new_status.png) ### Create event subscriber to verify state Now, you need to pass the information that the button has been selected to the list of applications to change the application status. Create another event subscriber that passes the information from the created form to the application list `src/Corporate/EventSubscriber/VerifyStateEventSubscriber.php`. ```php 'mapApplicationWorkflowForm', ApplicationWorkflowEvents::getStateEvent(self::VERIFY_STATE) => 'applicationVerify', ]; } public function mapApplicationWorkflowForm(MapApplicationWorkflowFormEvent $event): void { if ($event->getState() === self::VERIFY_STATE) { $form = $this->formFactory->create(VerifyType::class, $event->getData()); $event->setForm($form); } } public function applicationVerify(ApplicationWorkflowFormEvent $event): void { $data = $event->getData(); if (!is_array($data)) { return; } $applicationStateUpdateStruct = new ApplicationStateUpdateStruct($event->getApplicationState()->getId()); $applicationStateUpdateStruct->state = self::VERIFY_STATE; $this->applicationStateHandler->update($applicationStateUpdateStruct); $this->notificationHandler->success( /** @Desc("Application moved to Verification state") */ 'application.state.verify.notification', [], 'corporate_account_application' ); } } ``` In line 46, you can see that it handles changes to verify status. The subscriber only informs that the status has been changed (line 72). Now, if you click the **Verify** button during application review, the application gets **Verify** status. ![Verify status](https://doc.ibexa.co/en/saas/customer_management/img/cp_verify_status.png) # Create user registration form > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). Customize the registration form for new users in your site front end. You can create a registration form for users to your website by creating a Twig template or editing the existing registration form in YAML file. Follow the instructions below to create and customize templates for a registration form, and a registration confirmation page. First, make sure you [enabled user registration](https://doc.ibexa.co/en/saas/permissions/permission_use_cases/#register-users). ## Configure existing form In your configuration, under `allowed_field_definitions_identifiers` [configuration key](https://doc.ibexa.co/en/saas/administration/configuration/configuration/#configuration-files), specify the fields that should be part of your registration form. You can also define what kind of user you want to create under `user_type_identifier`, for example, frontend user. To learn more about available users, see [user types documentation](https://doc.ibexa.co/en/saas/users/user_registration/#user-types). ```yaml ibexa: system: default: user_registration: user_type_identifier: customer form: allowed_field_definitions_identifiers: - first_name - last_name - user_account ``` ## Add a form template Add the following configuration under the `ibexa.system..user_registration` [configuration key](https://doc.ibexa.co/en/saas/administration/configuration/configuration/#configuration-files): ```yaml ibexa: system: default: user_registration: templates: form: '@ibexadesign/user/registration_form.html.twig' confirmation: '@ibexadesign/user/registration_confirmation.html.twig' ``` This defines which templates are used for rendering the registration form and confirmation page. In the `templates/themes//user/registration_form.html.twig` create the template for registration form. Example registration form: ```html+twig {% extends no_layout is defined and no_layout == true ? view_base_layout : page_layout %} {% block content %}
    {{ form_start(form) }} {% for fieldForm in form.fieldsData %} {% set fieldIdentifier = fieldForm.vars.data.fieldDefinition.identifier %}
    {{ form_widget(fieldForm.value, { 'contentData': form.vars.data }) }}
    {%- do fieldForm.setRendered() -%} {% endfor %}
    {{ form_widget(form.register, {'attr': { 'class': 'btn btn-block btn-primary' }}) }}
    {{ form_end(form) }}
    {% endblock %} ``` In the `templates/themes//user/registration_confirmation.html.twig`, create the template for confirmation form. Example confirmation form: ```html+twig {% extends no_layout is defined and no_layout == true ? view_base_layout : page_layout %} {% block content %}

    Your account has been created

    Thank you for registering an account. You can now login.

    {% endblock %} ``` To add a link redirecting to the login form, in the page layout template, provide the following code: ```html+twig Register ``` # Data collection # Qualifio integration > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). Use Qualifio to collect customer data by creating interactive content. Editions: Experience Qualifio is a data collection tool. It gives you the ability to use the [Qualifio](https://qualifio.com/) tools to engage your audiences. You can use interactive content to build relationships and collect important data, for example, a list of recent orders, or personal information about customers. You can also integrate Qualifio with Ibexa Connect to create workflows. - [Install Qualifio](https://ez-systems-developer-documentation--3381.com.readthedocs.build/en/3381/qualifio/install_qualifio/): Install and configure Qualifio. - [Create Qualifio campaign](https://ez-systems-developer-documentation--3381.com.readthedocs.build/en/3381/qualifio/create_campaign/): Create a campaign with Qualifio. - [Integrate with Ibexa Connect](https://ez-systems-developer-documentation--3381.com.readthedocs.build/en/3381/qualifio/integrate_ibexa_connect/): Integrate Qualifio with Ibexa Connect. # Install Qualifio > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). Install and configure Qualifio. Editions: Experience Qualifio is a data collection tool. It enables you to engage your audiences by using the [Qualifio](https://qualifio.com/) tools. You can use interactive content to gather valuable data, for example, customer data or recent orders list, and create connections. For more information, see [Qualifio Developers documentation](https://developers.qualifio.com/docs/engage/). ## Enable Qualifio account To use Qualifio, you must make arrangements with Cohesivo to define the initial configuration. Ibexa team creates and provides user account. An invitation link is sent during the setup process. For more information, see [Qualifio in User Documentation](https://doc.ibexa.co/projects/userguide/en/6.0/qualifio/qualifio/#request-access). ## Install Qualifio Qualifio comes from v4.6.6 of Ibexa Experience. If you have different version, run the following command to install the bundle: ```bash composer require ibexa/engage ``` You can check for its presence by using the following command: ```bash composer show | grep "ibexa/engage" ``` This command adds to your project configuration files required for using Qualifio. ## Prepare configuration files In `config/packages` directory add the following `ibexa_connector_qualifio.yaml` [YAML configuration](https://doc.ibexa.co/en/saas/administration/configuration/configuration/#configuration-files): ```yaml ibexa_connector_qualifio: client_id: 1234 channel: 'd882c3f1-1b05-48c8-bc1d-c11a45e2c23a' feed_url: 'https://api.qualif.io/v1/campaignfeed/channels/d882c3f1-1b05-48c8-bc1d-c11a45e2c23a/json?clientId=1234' variable_map: form: shipping_address: first_name: 'firstname' last_name: 'lastname' email: 'email' street: 'address' postal_code: 'zip_code' locality: 'locality' country: 'country' phone_number: 'phone' billing_address: first_name: 'firstname' last_name: 'lastname' email: 'email' street: 'address' postal_code: 'zip_code' locality: 'locality' country: 'country' phone_number: 'phone' content: birthday: 'birthday' first_name: 'firstname' last_name: 'lastname' company: 'company' position: 'position' phone: 'phone' gender: 'gender' account: name: 'name' email: 'email' id: 'userid' remote_id: 'identifier' login: 'login' ``` - `client_id` - an identifier of the user. - `channel` - an UUID identifier format: a string of 30+ characters, divided by four hyphens, specific per publication channel. - `feed_url` - an URL link of the campaign feed. To create a campaign feed, follow the [Qualifio documentation](https://support.qualifio.com/hc/en-us/articles/360022954454-About-Campaign-Feeds). > **Note: Note** > > Ibexa configures the `channel` and `client_id` values so that the selections can be filled up automatically on Cohesivo side. > > The `feed_url` and `variable_map` values don't need to be set at the installation process. They're preconfigured and can be overwritten. # Create Qualifio campaign > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). Create a campaign with Qualifio. Editions: Experience [Campaign](https://doc.ibexa.co/projects/userguide/en/6.0/qualifio/qualifio/#campaign) is a set of concepts, divided into steps, that the user can configure. It can contain, for example, a welcome screen, an interaction element, a form step, and an exit screen. To create new campaign, you need to use Qualifio Manager. You can use Qualifio's existing templates and interactive elements, such as quizzes, pools, and forms, to create visually appealing, customized campaigns. Users can configure the backgrounds, themes, or designs, and set up a specific time frame for each campaign. Technically, each campaign has a unique campaign ID, that is automatically defined by the Qualifio platform when it's created. For more information about creating and managing campaigns, see [Qualifio documentation](https://support.qualifio.com/hc/en-us/categories/202280638-Campaigns). ## Publication channels Each campaign includes a minimum of one publication channel that you can choose from the three options the platform provides for publishing a campaign. For more information about publication channels, see [Publication channel](https://doc.ibexa.co/projects/userguide/en/6.0/qualifio/qualifio/#publication-channel) in User Documentation. ## Use Campaign block in Page Builder You can add [Campaign block](https://doc.ibexa.co/projects/userguide/en/6.0/content_management/block_reference/#campaign-block) in Page Builder to display campaign on the landing page. To select campaign, go to **Properties** tab. From the **Campaign** drop-down, choose campaign. This list includes all campaigns available on user's Qualifio account which are active or scheduled to launch in the future. You can set the dimensions of the field in which the campaign is displayed. To do it, insert width and height values in the proper fields. If size fields are blank, the system sets default template values. It's recommended to adjust them for better results. ![Campaign block](https://doc.ibexa.co/en/saas/qualifio/img/campaign_block.png "Campaign block") ## Embed campaign in the Rich text field You can embed campaign in the Rich text field with Campaign custom tag. To do it, insert **Campaign** content item in the Rich Text Field and choose campaign from the drop-down list. This list includes all campaigns available on user's Qualifio account which are active or scheduled to launch in the future. You can set the dimensions of the field in which the campaign is displayed. To do it, select units, and provide width and height values in the proper fields. If size fields are blank, the system sets default template values. It's recommended to adjust them for better results. ![Campaign custom tag](https://doc.ibexa.co/en/saas/qualifio/img/campaign_custom_tag.png "Campaign custom tag") # Integrate with Ibexa Connect > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). Integrate Qualifio with Ibexa Connect. Editions: Experience You can use [Ibexa Connect](https://doc.ibexa.co/projects/connect/en/latest/general/ibexa_connect/) to create workflows. Qualifio collects user data and passes it directly to Ibexa Connect. With this data, you can create scenarios, for example, to add a user to newsletter, or to specific user segment group. For more information, see [Ibexa Connect documentation](https://doc.ibexa.co/projects/connect/en/latest/). ## Use Ibexa Connect Webhooks provide a powerful way to transfer data between applications in real-time. You can use webhooks to connect Qualifio with Ibexa Connect - integration platform (iPaaS). This integration allows to collect data using Qualifio and then push it to another systems, such as CRMs, CDP, Marketing Automation platforms, or more. ### Get the webhook URL Use Qualifio App and scenario to get the webhook URL from Ibexa Connect. To set up a webhook in Ibexa Connect, follow the steps: 1. Log in to your Ibexa Connect account. 2. Go to **Scenarios** and click the plus button to create a new scenario. 3. Select **Receive participation data**. ![Create a scenario](https://doc.ibexa.co/en/saas/qualifio/img/create_scenario.png "Create a scenario") 4. Click **Create a webhook** and provide a name for the new webhook. 5. Click **Copy address to clipboard** to save the URL. ![Create a webhook](https://doc.ibexa.co/en/saas/qualifio/img/create_webhook.png "Create a webhook") ### Configure Qualifio The next step is to configure Qualifio. When a form submission event takes place, data can be sent through the obtained webhook URL. To do it, perform the following actions:: 1. Log in to your Qualifio account. 2. Go to **Engage** -> **Integrations** -> **Integrations** and select **Webhook**. 3. Paste the URL from the clipboard into **Webhook Host** field and click **Save**. ![Configure Qualifio](https://doc.ibexa.co/en/saas/qualifio/img/configure_qualifio.png "Configure Qualifio") 4. Then, go to **Engage** -> **Integrations** -> **Push rules** to define the default or specific rules for new campaign or website. Select the created webhook. # Multisite # Multisite > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). Multisite enables hosting multiple websites with different content, templates and configuration by using one repository. A multisite setup enables you to create more than one site in one installation of Cohesivo. Multisite configuration is done using [SiteAccesses](https://doc.ibexa.co/en/saas/multisite/siteaccess/siteaccess/index.md). To quickly set up new sites with predefined site templates, use [Site Factory](https://doc.ibexa.co/en/saas/multisite/site_factory/site_factory/index.md). - [SiteAccess](https://ez-systems-developer-documentation--3381.com.readthedocs.build/en/3381/multisite/siteaccess/siteaccess/): SiteAccesses enable you to provide separate configuration for each site in a multisite setup. - [Set up campaign SiteAccess](https://ez-systems-developer-documentation--3381.com.readthedocs.build/en/3381/multisite/set_up_campaign_siteaccess/): Create a special SiteAccess to host a campaign site with different content subtree. - [Set up translation SiteAccess](https://ez-systems-developer-documentation--3381.com.readthedocs.build/en/3381/multisite/set_up_translation_siteaccess/): Set up SiteAccesses to hold different language versions of a site. - [Multisite configuration](https://ez-systems-developer-documentation--3381.com.readthedocs.build/en/3381/multisite/multisite_configuration/): Configure SiteAccesses to serve different content in different layouts. - [Site Factory](https://ez-systems-developer-documentation--3381.com.readthedocs.build/en/3381/multisite/site_factory/site_factory/): Site Factory allows creating multiple sites (SiteAccesses) from the back office. - [Site Factory configuration](https://ez-systems-developer-documentation--3381.com.readthedocs.build/en/3381/multisite/site_factory/site_factory_configuration/): Configure Site Factory, including site skeletons. # Multisite configuration > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). Configure SiteAccesses to serve different content in different layouts. You can configure the available SiteAccesses under the `ibexa.siteaccess` [configuration key](https://doc.ibexa.co/en/saas/administration/configuration/configuration/#configuration-files). ## SiteAccess configuration ```yaml ibexa: siteaccess: list: [site, event] groups: site_group: [site] event_group: [event] default_siteaccess: site match: URIElement: 1 ``` ### SiteAccess groups `ibexa.siteaccess.groups` defines which groups SiteAccesses belong to. ```yaml ibexa: siteaccess: groups: site_group: [site] event_group: [event] ``` You can use groups when you want to use common settings for several SiteAccesses and avoid duplicating configuration. SiteAccess groups act like regular SiteAccesses as far as configuration is concerned. A SiteAccess can be part of several groups. SiteAccess configuration has always precedence over group configuration. #### `admin` SiteAccess The predefined `admin` SiteAccess in `admin_group` (configured in `config/packages/ibexa_admin_ui.yaml`) serves the back office. Don't remove this group. If you need a multisite setup with multiple back offices, add any additional administration SiteAccesses to `admin_group`. In cases where the sites are on separate databases, each needs its own [repository](https://doc.ibexa.co/en/saas/administration/configuration/repository_configuration/index.md) (including their own storage and search connection), var dir, [cache pool](https://doc.ibexa.co/en/saas/infrastructure_and_maintenance/cache/persistence_cache/#persistence-cache-configuration), and ideally also separate Varnish/Fastly configuration. > **Caution: Caution** > > Different SiteAccesses can only have different `var_dir` if they also have different repositories. Make sure there are no special or Unicode characters in your `var_dir` values. ### Default SiteAccess The `default_siteaccess` setting identifies which SiteAccess is used by default when no other SiteAccess matches. ```yaml ibexa: siteaccess: default_siteaccess: site ``` ### SiteAccess matching The `match` setting defines the rule or set of rules by which SiteAccesses are matched. For more information, see [SiteAccess matching](https://doc.ibexa.co/en/saas/multisite/siteaccess/siteaccess_matching/index.md). ```yaml ibexa: siteaccess: match: URIElement: 1 ``` ### SiteAccess name To create a better editorial experience, you can replace the SiteAccess code in the back office with a human-readable name of the website, for example `Company site` or `Summer Sale`. You can also translate SiteAccess names. Displayed names depend on the current back office language. To define translations or SiteAccess names, place them in YAML file with correct language code, for example `translations/ibexa_siteaccess.en.yaml`: ```yaml en: Company site fr: Company site France ``` ## Scope All SiteAccess-aware configuration is resolved depending on scope. The available scopes are: 1. `global` 2. SiteAccess 3. SiteAccess group 4. `default` `global` overrides all other scopes. If `global` isn't defined, the configuration then tries to match a SiteAccess, and then a SiteAccess group. Finally, if no other scope is matched, `default` is applied. In short: if you want a match that always applies, regardless of SiteAccesses, use `global`. To define a fallback, use `default`. ```yaml ibexa: system: global: # If set, this value is used regardless of any other configuration site: # This is used for the 'site' SiteAccess site_group: # This is overwritten by the SiteAccess above, since the SiteAccess has precedence default: # This value is only used if there is no setting for global scope, SiteAccess or SiteAccess group ``` `global` and `default` scopes include the `admin` SiteAccess, which is responsible for the back office. For example, the following configuration defines both the front template for articles and the template used in the back office, unless you configure other templates for a specific SiteAccess or SiteAccess group: ```yaml ibexa: system: default: content_view: full: article: template: full/article.html.twig match: Identifier\ContentType: [article] ``` ### SiteAccesses and Page Builder (Experience) To define which SiteAccesses are available in the submenu in Page Builder, use the following configuration: ```yaml ibexa: system: admin: page_builder: siteaccess_list: [site, de, fr, no] de: page_builder: siteaccess_list: [site, de] ``` If you're using multiple domains, list all domains for an admin SiteAccess under `siteaccess_hosts`: ```yaml ibexa: system: admin: page_builder: siteaccess_list: [site, de, fr, no] siteaccess_hosts: - my_domain.com - another_domain.org ``` > **Caution: SiteAccess with separate admin domain** > > If an admin SiteAccess in your installation uses a different domain than the front SiteAccesses, be sure to use SSL (https protocol). Otherwise, you cannot preview content in Page Builder from the back office. #### SiteAccess switching in Page Builder If you need to change between SiteAccesses in Site mode, don't use any functions in the page itself (for example, a language switcher). This may cause unexpected errors. Instead, switch between SiteAccesses with the SiteAccess bar above the page. ## Location tree You can restrict SiteAccesses to different parts of the content tree. When you do it, only the selected location and its descendants are reachable from this SiteAccess. Configure this under the `ibexa.systems..content.tree_root` [configuration key](https://doc.ibexa.co/en/saas/administration/configuration/configuration/#configuration-files), for example: ```yaml ibexa: system: : content: tree_root: location_id: 42 excluded_uri_prefixes: [/media/, /images/] index_page: /EventFrontPage ``` - `location_id` defines the location ID of the content root for the SiteAccess. - `excluded_uri_prefixes` defines which URIs ignore the root limit set by using `location_id`. In the example above, to access the Media and Images folders, you can use their own URI, even though they're outside the location provided in `content.tree_root.location_id`. It's an array of prefixes. So, for example, `[/media]` would also exclude `/mediation` from root limit. - `index_page` is the page shown when you access the root index `/`. > **Note: Note** > > Prefixes aren't case sensitive. Leading slashes (`/`) are automatically trimmed internally, so they can be ignored. > **Tip: Tip** > > For an example of a multisite configuration, see [Set up campaign SiteAccess](https://doc.ibexa.co/en/saas/multisite/set_up_campaign_siteaccess/index.md). # SiteAccess > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). SiteAccesses enable you to provide separate configuration for each site in a multisite setup. A SiteAccess is a set of configuration settings that the application uses when you access the site through a specific address. When the user visits the site, the system analyzes the URI and compares it to rules specified in the configuration. If it finds a set of fitting rules, this SiteAccess is used. Each SiteAccess can have different: - [templates and designs](https://doc.ibexa.co/en/saas/templating/design_engine/design_engine/index.md) - [languages](https://doc.ibexa.co/en/saas/multisite/set_up_translation_siteaccess/index.md) - [tree roots](https://doc.ibexa.co/en/saas/multisite/multisite_configuration/#location-tree) - [repositories](https://doc.ibexa.co/en/saas/infrastructure_and_maintenance/cache/persistence_cache/#multi-repository-setup) - [recommendations](https://doc.ibexa.co/en/saas/recommendations/raptor_integration/connector_installation_configuration/#siteaccess-aware-configuration) Many other settings in the application are also configured per SiteAccess (also known as "SiteAccess-aware"). > **Tip: Tip** > > When possible, always use semantic (SiteAccess-aware) configuration. Manually editing internal settings is possible, but at your own risk, as unexpected behavior can occur. - [SiteAccess matching](https://ez-systems-developer-documentation--3381.com.readthedocs.build/en/3381/multisite/siteaccess/siteaccess_matching/): Use SiteAccess matchers to control which site is served when and to which user. - [SiteAccess-aware configuration](https://ez-systems-developer-documentation--3381.com.readthedocs.build/en/3381/multisite/siteaccess/siteaccess_aware_configuration/): Make sure your custom development's configuration can be used with SiteAccesses. - [Injecting SiteAccess](https://ez-systems-developer-documentation--3381.com.readthedocs.build/en/3381/multisite/siteaccess/injecting_siteaccess/): Inject the SiteAccess service to get SiteAccess information in your custom PHP code. # SiteAccess matching > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). Use SiteAccess matchers to control which site is served when and to which user. To be usable, every SiteAccess must be matched by one of configured matchers. By default, all SiteAccesses are matched using `URIElement: 1`. You can configure SiteAccess matchers under the `ibexa.siteaccess.match` [configuration key](https://doc.ibexa.co/en/saas/administration/configuration/configuration/#configuration-files): ```yaml ibexa: siteaccess: list: [site, event] groups: site_group: [site, event] default_siteaccess: site match: Map\URI: site: site campaign: event ``` `ibexa.siteaccess.match` can contain multiple matchers. The first matcher succeeding always wins, so be careful when using catch-all matchers like `URIElement`. In the following example, `Compound\LogicalAnd` is placed before the `Map\Host` for `my.site/corporate` to be reachable: ```yaml ibexa: siteaccess: match: Compound\LogicalAnd: corporate: matchers: Map\URI: corporate: true Map\Host: my.site: true match: corporate Map\Host: my.site: mysite ``` If the matcher class doesn't start with a backslash (`\`), it's relative to `Ibexa\Core\MVC\Symfony\SiteAccess\Matcher` (for example, `Map\URI` refers to `Ibexa\Core\MVC\Symfony\SiteAccess\Matcher\Map\URI`) You can specify [custom matchers](#custom-matchers) by using a fully qualified class name (for example, `\My\SiteAccess\Matcher`) or a service identifier (for example, `@my_matcher_service`). In the case of a fully qualified class name, the matching configuration is passed in the constructor. In the case of a service, it must implement `Ibexa\Bundle\Core\SiteAccess\Matcher`. The matching configuration is passed to `setMatchingConfiguration()`. ## Available SiteAccess matchers - [`URIElement`](#urielement) - [`URIText`](#uritext) - [`HostElement`](#hostelement) - [`HostText`](#hosttext) - [`Map\Host`](#maphost) - [`Map\URI`](#mapuri) - [`Map\Port`](#mapport) - [`Ibexa\SiteFactory\SiteAccessMatcher`](#ibexasitefactorysiteaccessmatcher) ### `URIElement` Maps a URI element to a SiteAccess. In configuration, provide the element number you want to match (starting from 1). ```yaml ibexa: siteaccess: match: URIElement: 2 ``` > **Note: Note** > > When you use a value > 1, the matcher concatenates the elements with `_`. Example URI `/my_site/company/pages` matches SiteAccess `my_site_company`. ### `URIText` Matches URI using prefix and suffix sub-strings in the first URI segment. In configuration, provide the prefix and/or suffix (neither is required). ```yaml ibexa: siteaccess: match: URIText: prefix: main- suffix: /company ``` Example URI `/main-event/company/page` matched SiteAccess `event`. ### `HostElement` Maps an element in the host name to a SiteAccess. In configuration, provide the element number you want to match (starting from 1). ```yaml ibexa: siteaccess: match: HostElement: 2 ``` Example host name `www.example.com` matches SiteAccess `example`. ### `HostText` Matches a SiteAccess in the host name, using pre and/or post sub-strings. In configuration, provide the prefix and/or suffix (none are required). ```yaml ibexa: siteaccess: match: HostText: prefix: www. suffix: .com ``` Example host name `www.example.com` matches SiteAccess `example`. ### `Map\Host` Maps a host name to a SiteAccess. In configuration, provide a hash map of host/SiteAccess. ```yaml ibexa: siteaccess: match: Map\Host: www.page.com: event adm.another-page.fr: event_admin ``` Example host name `www.page.com` matches SiteAccess `event`. > **Note: Note** > > If you encounter problems with the `Map\Host` matcher, make sure that your installation is properly configured to use token-based authentication. ### `Map\URI` Maps a URI to a SiteAccess. In configuration, provide a hash map of URI/SiteAccess. ```yaml ibexa: siteaccess: match: Map\URI: campaign: event site: site ``` Example URI `/campaign/general/articles` matches SiteAccess `event`. ### `Map\Port` Maps a port to a SiteAccess. In configuration, provide a hash map of Port/SiteAccess. ```yaml ibexa: siteaccess: match: Map\Port: 80: event 8080: site ``` Example URL `http://my_site.com:8080/content` matches SiteAccess `site`. ### `Ibexa\SiteFactory\SiteAccessMatcher` (Experience) Enables the use of [Site Factory](https://doc.ibexa.co/en/saas/multisite/site_factory/site_factory/index.md). Doesn't take any parameters in configuration: ```yaml ibexa: siteaccess: match: '@Ibexa\SiteFactory\SiteAccessMatcher': ~ ``` ## Custom matchers Beside the built-in matchers, you can also use your own services to match SiteAcceses: ```yaml ibexa: siteaccess: list: [site] groups: site_group: [site] default_siteaccess: site match: '@App\Matcher\MySiteaccessMatcher': ~ ``` The service must be tagged with `ibexa.site_access.matcher` and must implement `Ibexa\Bundle\Core\SiteAccess\Matcher` (and `Ibexa\Core\MVC\Symfony\SiteAccess\VersatileMatcher` if you want to use compound logical matchers). ## Combining SiteAccess matchers You can combine more than one SiteAccess matcher to match more complex situations, for example: - `http://example.com/en` matches `site_en` (match host example.com and the `en` URI element) - `http://example.com/fr` matches `site_fr` (match host example.com and the `fr` URI element) - `http://admin.example.com` matches `site_admin` (match host admin.example.com) To combine matchers, use compound logical matchers: - `Compound\LogicalAnd` - `Compound\LogicalOr` Each compound matcher specifies two or more sub-matchers. A rule applies if all the matchers combined with the logical matcher are positive. To get the result above, you need to combine `Map\Host` and `Map\Uri` using `LogicalAnd`. When both the URI and host match, the SiteAccess configured with `match` is used. ```yaml ibexa: siteaccess: match: Compound\LogicalAnd: # You don't need to specify matching values (true is enough). site_en: matchers: Map\URI: en: true Map\Host: example.com: true match: site_en site_fr: matchers: Map\URI: fr: true Map\Host: example.com: true match: site_fr Map\Host: admin.example.com: site_admin ``` When using `Compound\LogicalAnd`, all inner matchers must match. All matchers must implement `VersatileMatcher`. When using `Compound\LogicalOr`, the first inner matcher succeeding wins. ## Matching by request header You can define which SiteAccess to use by setting an `X-Siteaccess` header in your request. This can be useful for REST requests. In such a case, `X-Siteaccess` must be the SiteAccess name (for example, `site` or `en`). ## Matching by environment variable You can also define which SiteAccess to use directly by using the `EZPUBLISH_SITEACCESS` environment variable. This is recommended if you want to get performance gain since no matching logic is done in this case. You can define this environment variable directly in web server configuration: ```vcl # This configuration assumes that mod_env is activated DocumentRoot "/path/to/ibexa/web/folder" ServerName example.com ServerAlias www.example.com SetEnv EZPUBLISH_SITEACCESS demo_site ``` > **Tip: Tip** > > You can configure the variable by using the PHP-FPM configuration file. For more information, see [PHP-FPM documentation](https://www.php.net/manual/en/install.fpm.configuration.php). > **Note: Precedence** > > The precedence order for SiteAccess matching is the following (the first matched wins): > > 1. Request header > 1. Environment variable > 1. Configured matchers # SiteAccess-aware configuration > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). Make sure your custom development's configuration can be used with SiteAccesses. The [Symfony Config component](https://symfony.com/doc/7.4/components/config.html) makes it possible to define semantic configuration, exposed to the end developer. This configuration is validated by rules you define, for example, validating type (string, array, integer, boolean, and more). Usually, after it's validated and processed, this semantic configuration is then mapped to internal *key/value* parameters stored in the service container. Cohesivo uses this for its core configuration, but adds another configuration level, the SiteAccess. For each defined SiteAccess, you need to be able to use the same configuration tree to define SiteAccess-specific config. These settings then need to be mapped to SiteAccess-aware internal parameters that you can retrieve with the [ConfigResolver](https://doc.ibexa.co/en/saas/administration/configuration/dynamic_configuration/#configresolver). For this, internal keys need to follow the format `..`. where: - `namespace` is specific to your app or bundle - `scope` is the SiteAccess, SiteAccess group, `default` or `global` - `parameter_name` is the actual setting *identifier* For more information about the ConfigResolver, namespaces and scopes, see [configuration basics](https://doc.ibexa.co/en/saas/administration/configuration/configuration/index.md). > **Tip: Repository-aware configuration** > > If you need to use different settings per repository, not per SiteAccess, see [Repository-aware configuration](https://doc.ibexa.co/en/saas/administration/configuration/repository_configuration/#repository-aware-configuration). The example below assumes you're using an `Acme\ExampleBundle`. Remember to register the bundle by adding it to `config/bundles.php`: ```php return [ // ... Acme\ExampleBundle\AcmeExampleBundle::class => ['all' => true], ]; ``` ## Parsing semantic configuration To parse semantic configuration, create a `Configuration` class which extends `Ibexa\Bundle\Core\DependencyInjection\Configuration\SiteAccessAware\Configuration` and then extend its `generateScopeBaseNode()` method: ```php The tree builder */ public function getConfigTreeBuilder(): TreeBuilder { $treeBuilder = new TreeBuilder('acme_example'); $rootNode = $treeBuilder->getRootNode(); // $systemNode is the root of SiteAccess-aware settings. $systemNode = $this->generateScopeBaseNode($rootNode); $systemNode ->scalarNode('name')->isRequired()->end() ->arrayNode('custom_setting') ->children() ->scalarNode('string')->end() ->integerNode('number')->end() ->booleanNode('enabled')->end() ->end() ->end(); return $treeBuilder; } } ``` > **Note: Note** > > Default name for the *SiteAccess root node* is `system`, but you can customize it. To do this, pass the name you want to use as a second argument of `$this->generateScopeBaseNode()`. This enables you to use the following SiteAccess-aware configuration: ```yaml acme_example: system: : name: name_1 custom_setting: number: 456 enabled: true : name: name_2 custom_setting: string: value number: 123 enabled: false ``` ## Mapping to internal settings Semantic configuration must always be mapped to internal key/value settings within the service container. You usually do it in the [service container](https://doc.ibexa.co/en/saas/api/php_api/php_api/#service-container) extension. ```php getConfiguration($configs, $container); $config = $this->processConfiguration($configuration, $configs); $loader = new Loader\YamlFileLoader($container, new FileLocator(self::ACME_CONFIG_DIR)); $loader->load('default_settings.yaml'); $processor = new ConfigurationProcessor($container, 'acme_example'); $processor->mapConfig( $config, // Any kind of callable can be used here. // It is called for each declared scope/SiteAccess. static function ($scopeSettings, $currentScope, ContextualizerInterface $contextualizer): void { // Maps the "name" setting to "acme_example.<$currentScope>.name" container parameter // It is then possible to retrieve this parameter through ConfigResolver in the application code: // $helloSetting = $configResolver->getParameter( 'name', 'acme_example' ); $contextualizer->setContextualParameter('name', $currentScope, $scopeSettings['name']); } ); // Now map "custom_setting" and ensure the key defined for "my_siteaccess" overrides the one for "my_siteaccess_group" // It is done outside the closure as it's needed only once. $processor->mapConfigArray('custom_setting', $config); } /** @param array $config */ #[\Override] public function getConfiguration(array $config, ContainerBuilder $container): Configuration { return new Configuration(); } } ``` You can also map simple settings by calling `$processor->mapSetting()`, without having to call `$processor->mapConfig()` with a callable. ```php $processor = new ConfigurationProcessor($container, 'acme_example'); $processor->mapSetting('name', $config); ``` > **Caution: Important** > > Always ensure you have defined and loaded default settings. In `@AcmeExampleBundle/Resources/config/default_settings.yaml`: ```yaml parameters: acme_example.default.name: name_1 acme_example.default.custom_setting: string: ~ number: 0 enabled: false ``` ### Merging hash values between scopes When you define a hash as semantic config, you sometimes don't want the SiteAccess settings to replace the default or group values, but enrich them by appending new entries. This is possible by using `$processor->mapConfigArray()`, which you must call outside the closure (before or after), so that it's called only once. ```php $processor->mapConfigArray('custom_setting', $config); ``` Consider the following default config in `default_settings.yaml`: ```yaml parameters: acme_example.default.custom_setting: string: ~ os_types: [windows] number: 0 enabled: false language: php ``` And then this semantic configuration in `config/packages/acme.yaml`: ```yaml acme_example: system: siteaccess_group: custom_setting: string: value number: 123 # Assuming "siteaccess1" is part of "siteaccess_group" siteaccess1: custom_setting: os_types: [linux, macos] number: 456 enabled: true language: javascript ``` By calling `mapConfigArray()` you can get the following end configuration, where keys defined for `custom_setting` in default/group/SiteAccess scopes are merged: ```yaml parameters: acme_example.siteaccess1.custom_setting: string: value os_types: [linux, macos] number: 456 enabled: true language: javascript ``` #### Merging from second level In the example above, entries were merged in respect to the scope order of precedence. However, because you defined the `os_types` key for `siteaccess1`, it completely overrode the default value, because the merge process is done only at the first level. You can add another level by passing `ContextualizerInterface::MERGE_FROM_SECOND_LEVEL` as the third argument to `$contextualizer->mapConfigArray()`: ```php $contextualizer = $processor->getContextualizer(); $contextualizer->mapConfigArray('custom_setting', $config, ContextualizerInterface::MERGE_FROM_SECOND_LEVEL); ``` When you use `ContextualizerInterface::MERGE_FROM_SECOND_LEVEL` with the configuration above, you get the following result: ```yaml parameters: acme_example.siteaccess1.custom_setting: string: value os_types: [windows, linux, macos] number: 456 enabled: true language: javascript ``` There is also another option, `ContextualizerInterface::UNIQUE`, that ensures the array setting has unique values. It only works on normal arrays, not hashes. > **Note: Note** > > Merge isn't recursive. Only second level merge is possible by using `ContextualizerInterface::MERGE_FROM_SECOND_LEVEL` option. ### Dedicated mapper object Instead of passing a callable to `$processor->mapConfig()`, you can pass an instance of `Ibexa\Bundle\Core\DependencyInjection\Configuration\SiteAccessAware\ConfigurationMapperInterface`. This can be useful if you have a lot of configuration to map and don't want to pollute your service container extension class (it's better for maintenance). #### Merging hash values between scopes You should not use `$contextualizer->mapConfigArray()` within the scope loop, like for simple values. When using a closure/callable, you usually call it before or after `$processor->mapConfig()`. For mapper objects, you can use a dedicated interface: `HookableConfigurationMapperInterface`, which defines two methods: `preMap()` and `postMap()`. # Injecting SiteAccess > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). Inject the SiteAccess service to get SiteAccess information in your custom PHP code. The [service container](https://doc.ibexa.co/en/saas/api/php_api/php_api/#service-container) exposes the SiteAccess through the `Ibexa\Core\MVC\Symfony\SiteAccess\SiteAccessService` service, which fulfills the `Ibexa\Core\MVC\Symfony\SiteAccess\SiteAccessServiceInterface` contract. This means you can inject it into any custom service constructor, type hinting that contract. You can get the current SiteAccess from that service by calling the `SiteAccessServiceInterface::getCurrent` method. For example, define a service which depends on the Repository's ContentService and the SiteAccessService. ```yaml services: App\MyService: arguments: ['@Ibexa\Core\MVC\Symfony\SiteAccess\SiteAccessService'] ``` ```php For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). Create a special SiteAccess to host a campaign site with different content subtree. The following example shows how to set up a special `campaign` SiteAccess. This SiteAccess serves a site devoted to a special campaign, separate from the main company website (`site` SiteAccess). The `campaign` site uses a different part of the content tree than the main site, but shares some media files with it. ## Configure SiteAccesses First, in SiteAccess configuration, add the `campaign` SiteAccess to the list under the `ibexa.siteaccess` [configuration key](https://doc.ibexa.co/en/saas/administration/configuration/configuration/#configuration-files): ```yaml ibexa: siteaccess: list: [site, campaign] groups: site_group: [site, campaign] default_siteaccess: site match: Map\URI: summer-sale: campaign site: site ``` The `match` setting ensures that when a visitor accesses the `/summer-sale` URI, they see the `campaign` SiteAccess. ## Set root folder Next, with the following content structure, you need to separate the "Campaign" folder as root for the new site: ![Content structure](https://doc.ibexa.co/en/saas/multisite/img/config_content_structure.png "Content structure") To do it, set the root level for `campaign` to access the "Campaign" Location and its sub-items only: ```yaml ibexa: system: campaign: content: tree_root: # LocationId of "Campaign" location_id: 57 ``` Thanks to this configuration, you can access `/campaign/Articles/Article2`, but not `/campaign/General/Articles/Article1`. ## Reuse content Finally, reuse some content between sites, for example "Logos" from "Images/Media". You can allow the `campaign` site to access them, even though they're in a different part of the tree, via [`excluded_uri_prefixes`](https://doc.ibexa.co/en/saas/multisite/multisite_configuration/#location-tree): ```yaml ibexa: system: campaign: content: tree_root: location_id: 57 excluded_uri_prefixes: [ /media/images/logos/ ] ``` Now, when you use the `campaign` SiteAccess, you can reach `/campaign/Media/Images/Logos`, despite the fact that it's not a sub-item of the "Campaign" location. As a next step, you can configure different [designs](https://doc.ibexa.co/en/saas/templating/design_engine/design_engine/index.md) for the two SiteAccesses. # Set up translation SiteAccess > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). Set up SiteAccesses to hold different language versions of a site. One of common uses for multisite installations is serving different language versions of a website. To do this, set up multiple SiteAccesses, each corresponding to one language. Proper configuration means avoiding duplicate content that could affect SEO. ## Add a language First, add a new language for the whole installation. > **Tip: Tip** > > For more details, see [Languages](https://doc.ibexa.co/en/saas/multisite/languages/languages/index.md). 1. In the back office, go to **Admin** -> **Languages**. 2. Click **Create a new language** and provide the language name and code (examples below use French with `fre-FR`). 3. After creating the new language, refresh the assets by running: ```bash yarn encore ``` ## Configure SiteAccesses Next, configure a new SiteAccess to match the newly-configured language. The most typical setup for a site with translated content is to map the base of the domain to one language and use the first segment of the URI to match to translations. For example: - `www.mysite.com` for English site - `www.mysite.com/fr` for French site To achieve this you need to create a new SiteAccess in configuration under the `ibexa.siteaccesses` [configuration key](https://doc.ibexa.co/en/saas/administration/configuration/configuration/#configuration-files). Add the `fr` SiteAccess to list of all SiteAccesses and it to the common `site_group`. This group is used for sharing settings such as API keys, cache locations, and more. ```yaml ibexa: siteaccess: list: [site, fr] groups: site_group: [site, fr] ``` Under the `ibexa.system` key, add the new SiteAccess. Indicate that they're meant for translations under `site_group.translation_siteaccesses`: ```yaml ibexa: system: site_group: # ... translation_siteaccesses: [fr] fr: languages: [fre-FR, eng-GB] site: languages: [eng-GB] ``` With this configuration, the main English site displays content in English and ignores French content. The French site displays content in French, but also in English, if it doesn't exist in French. Clear the cache by running: `php bin/console cache:clear`. ## Set permissions By default, the Anonymous user role doesn't have permissions for new SiteAccesses. As a next step, allow Anonymous users to read content on the new SiteAccesses: 1. In the back office, go to **Admin** -> **Roles**. 2. Click the **Anonymous** role. 3. Edit the **Limitations** of the module `user`, select both SiteAccesses and click **Update**. 4. Clear the cache by running: `php bin/console cache:clear`. You can now start translating content. When you reload the site, access a translated content item through both SiteAccesses to see the difference, for example: `/` and `/fr/`. # Site Factory > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). Site Factory allows creating multiple sites (SiteAccesses) from the back office. Editions: Experience Site Factory is a site management interface, integrated with the back office. It enables you to configure new sites without editing [YAML-based SiteAccess configuration](https://doc.ibexa.co/en/saas/multisite/multisite_configuration/index.md). > **Note: Note** > > A SiteAccess that you define for a site by following the [configuration](https://doc.ibexa.co/en/saas/multisite/multisite_configuration/index.md) is always treated with higher priority than a SiteAccess created by using the Site Factory. For example, if you define a French site within a YAML file, and then create a site that uses the `fr` path in Site Factory, matchers ignore the second site. Site Factory is disabled by default after installation. If you plan to use Site Factory, you need to enable and configure it. To enable or disable Site Factory, follow: - [Enable Site Factory section](#enable-site-factory) - [Disable Site Factory section](#disable-site-factory) ## Enable Site Factory To enable Site Factory, set the `ibexa_site_factory.enabled` [configuration key](https://doc.ibexa.co/en/saas/administration/configuration/configuration/#configuration-files) to `true`. ### Configure designs Next, configure Site Factory by adding empty SiteAccess groups. At least one empty group is required. The number of empty SiteAccess groups must be equal to the number of templates that you want to have when you create the new site. In this example, you add two SiteAccess groups (`example_site_factory_group_1` and `example_site_factory_group_2`) that correspond to the two templates (`site1` and `site2`) that you add in the next step. Add the groups under the `ibexa.siteaccess` [configuration key](https://doc.ibexa.co/en/saas/administration/configuration/configuration/#configuration-files): ```yaml ibexa: siteaccess: # ... groups: site_group: [import, site] storefront_group: [site] corporate_group: [corporate] example_site_factory_group_1: [ ] example_site_factory_group_2: [ ] system: example_site_factory_group_1: example_site_factory_group_2: ``` Uncomment the SiteAccess matcher (`Ibexa\SiteFactory\SiteAccessMatcher`): ```yaml ibexa: siteaccess: match: '@Ibexa\SiteFactory\SiteAccessMatcher': ~ ``` Next, add the [design engine](https://doc.ibexa.co/en/saas/templating/design_engine/design_engine/index.md) configuration for new specific designs and their theme lists: ```yaml ibexa_design_engine: design_list: example_1: [example_1_theme] example_2: [example_2_theme] ``` Finally, configure designs for empty SiteAccess groups: ```yaml ibexa: system: example_site_factory_group_1: design: example_1 example_site_factory_group_2: design: example_2 ``` ### Add site template configuration Add thumbnails and names for your site templates: ```yaml ibexa_site_factory: templates: site1: siteaccess_group: example_site_factory_group_1 name: Example site 1 thumbnail: /path/to/image/example-thumbnail_1.png site2: siteaccess_group: example_site_factory_group_2 name: Example site 2 thumbnail: /path/to/image/example-thumbnail_2.png ``` You can check the results of your work in the back office by going to **Site management** and selecting **Sites**. There, you should be able to add a new site and choose a design for it. ### Define domains To be able to see your site online, you need to define a domain for it. > **Caution: Define domain for production environment** > > These steps are for `dev` environment only. If you want to define domains in production environment, you need to configure Apache or Nginx by yourself. In the `.env` file change line 2 to: `COMPOSE_FILE=doc/docker/base-dev.yml:doc/docker/multihost.yml` Take a look into the `doc/docker/multihost.yml` file. Here you can define domains. To add a new domain, add it in `command:` and under frontend and backend aliases as shown in the example below: ```yaml services: web: command: /bin/bash -c "cd /var/www && cp -a doc/nginx/ibexa_params.d /etc/nginx && bin/vhost.sh --host-name=site.example.com --host-alias='admin.example.com test.example.com' --template-file=doc/nginx/vhost.template > /etc/nginx/conf.d/default.conf && nginx -g 'daemon off;'" networks: frontend: aliases: - site.example.com - admin.example.com - test.example.com backend: aliases: - site.example.com - admin.example.com - test.example.com ``` Next, you must define the domains in `etc/hosts`: `0.0.0.0 site.example.com admin.example.com test.example.com www.admin.example.com` Then, run `docker-compose up`: ```bash export COMPOSE_FILE="doc/docker/base-dev.yml:doc/docker/multihost.yml" docker-compose up ``` Your sites should be now visible under: - `http://site.example.com:8080/` - `http://admin.example.com:8080/` - `http://localhost:8080/` - `http://test.example.com:8080/` ![Site Factory enabled](https://doc.ibexa.co/en/saas/multisite/img/site_factory_site_list.png "Site Factory enabled") ### Define site directory You can adjust the place where the directory of the new site is created (location with ID 2 by default). To do it, go to configuration files and under the `ibexa.system..site_factory` [configuration key](https://doc.ibexa.co/en/saas/administration/configuration/configuration/#configuration-files) add the following parameter: ```yaml ibexa: system: default: site_factory: sites_location_id: 42 ``` Now, all new directories are created under "Cohesivo". ### Provide access The Site Factory is set up, now you can provide sufficient permissions to the users. Set the below policies to allow users to: - `site/view` - enter the Site Factory interface - `site/create` - create sites - `site/edit` - edit sites - `site/change_status` - change status of the public accesses to `Live` or `Offline` - `site/delete` - delete sites For full documentation on how permissions work and how to set them up, see [the permissions section](https://doc.ibexa.co/en/saas/permissions/permissions/index.md). To learn how to use Site Factory, see [User Documentation](https://doc.ibexa.co/projects/userguide/en/6.0/website_organization/work_with_sites/). ## Disable Site Factory Enabled Site Factory may cause following performance issues: - [ConfigResolver](https://doc.ibexa.co/en/saas/administration/configuration/dynamic_configuration/#configresolver) looks for SiteAccesses in the database - Site Factory matchers are connected to the database in search for new SiteAccesses You can disable Site Factory to boost ConfigResolver performance. Keep in mind that with disabled Site Factory you're unable to add new sites or use existing ones. 1. In `config/packages/ibexa_site_factory.yaml` change `enabled` to `false`. 2. In `config/packages/ibexa.yaml` comment the `ibexa.siteaccess.match: '@Ibexa\SiteFactory\SiteAccessMatcher': ~` if it's uncommented. 3. Remove separate connection to database in `config/packages/doctrine.yaml`. ```yaml doctrine: dbal: connections: # ... # This connection is dedicated for SiteFactory to avoid known issues site_factory: ``` 4. Remove separate cache pool in `config/packages/cache.yaml`. ```yaml framework: cache: # ... pools: # This pool should be used only by SiteFactory bundle site_factory_pool: ``` The Site Factory should be disabled. # Site Factory configuration > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). Configure Site Factory, including site skeletons. Editions: Experience ## Parent location When working with the [Site Factory](https://doc.ibexa.co/en/saas/multisite/site_factory/site_factory/index.md), you can define the parent location for a new site in the configuration. Each new site is created in the designated location. To define a parent location, add a new configuration key to the site template definition. Each template is assigned to its own location. This can be either a location ID (for example, `62`), or a recommended remote location ID (for example, `1548b8cd8dd4c6b5082e566615d45e91`). Add the configuration key to your template under the `ibexa_site_factory` [configuration key](https://doc.ibexa.co/en/saas/administration/configuration/configuration/#configuration-files): ```yaml ibexa_site_factory: templates: site1: siteaccess_group: example_site_factory_group_1 name: example_site_1 thumbnail: /path/to/image/example-thumbnail_1.png parent_location_id: 62 site2: siteaccess_group: example_site_factory_group_2 name: example_site_2 thumbnail: /path/to/image/example-thumbnail_2.png parent_location_remote_id: 1548b8cd8dd4c6b5082e566615d45e91 ``` Now, you can see the path to the new site's parent location under design selection. If you have sufficient permissions, you can change the defined location during site creation. If the parent location isn't defined, you have to choose it from Universal Discovery Widget. ## Site skeletons The Site skeleton enables you to copy an entire content structure of the site design to the defined location. Site skeleton copying is a one-off operation, it only happens during the site creation process. After that, you cannot copy the Site skeleton again, for example in the edit view. You can create as many skeletons as you need and assign them to templates. Remember that one template can only have one Site skeleton. If the design doesn't have a defined Site skeleton, a directory of the new site is created in a standard Site Factory process. To define a Site skeleton, add the `site_skeleton_id` or `site_skeleton_remote_id` key to the site template definition. This can be either a location ID (for example, `5966`), or a remote location ID (for example, `3bed95afb1f8126f06a3c464e461e1ae66`). ```yaml ibexa_site_factory: templates: site1: siteaccess_group: example_site_factory_group_1 name: example_site_1 thumbnail: /path/to/image/example-thumbnail_1.png site_skeleton_id: 5966 site2: siteaccess_group: example_site_factory_group_2 name: example_site_2 thumbnail: /path/to/image/example-thumbnail_2.png site_skeleton_remote_id: 3bed95afb1f8126f06a3c464e461e1ae66 ``` Now, you can choose a design with a defined Site skeleton, and decide if you want to use its skeleton by toggling **Generate site using site skeleton**. ## User group skeletons With user group skeletons you can define policies and limitations that apply to selected groups of users who can access the site. You can create many user group skeletons and associate them with many templates. One template can have many user group skeletons assigned. To create a user group skeleton, first go to **Admin** -> **Site skeletons** and add a user group to the list of available skeletons. Then, review the detailed information of the newly created user group skeleton, copy the location ID or the Location remote ID, and add a configuration key to the site template definition: ```yaml ibexa_site_factory: templates: : # ... user_group_skeleton_ids: [ , , ... ] user_group_skeleton_remote_ids: [ , , ... ] ``` Manage the permissions associated to the user group skeleton by [assigning roles](https://doc.ibexa.co/projects/userguide/en/6.0/permission_management/work_with_permissions/#assign-a-role-to-users). Make sure that the roles that you assign to the user group skeleton don't contain location-based limitations. User group skeletons cannot contain individual user content items either. User group skeletons are retained after deleting the site. ## Automatic update of roles Role definitions can contain user/login policies with limitations that limit user access to certain sites. To avoid the need to add the new SiteAccess to limitations for all roles, you can decide that the roles you select are automatically updated when the site is created, updated, or deleted. Under the `ibexa_site_factory` [configuration key](https://doc.ibexa.co/en/saas/administration/configuration/configuration/#configuration-files), add a list of roles which should have access to the frontend when a site is created in Site Factory, for example: ```yaml ibexa_site_factory: # ... enabled: true update_roles: [Anonymous, Administrator] ``` For more information about roles and policies, see [Permissions](https://doc.ibexa.co/en/saas/permissions/permissions/index.md). # Languages > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). You can create multiple language versions (translations) of content and serve different language versions of your site with the help of SiteAccesses. ## Language versions Cohesivo offers the ability to create multiple language versions (translations) of a content item. Translations are created per version of the item, so each version of the content can have a different set of translations. A version always has at least one translation which by default is the *initial/main* translation. Further versions can be added, but only for languages that have previously been [added to the global translation list](#adding-available-languages), that is a list of all languages available in the system. The maximum number of languages in the system is 62. Different translations of the same content item can be edited separately. This means that different users can work on translations into different languages at the same time. Each version, including a draft, contains all the existing translations. However, even if work on a draft takes time and other translations are updated in the meantime, publishing the draft doesn't overwrite later modifications. ### Adding available languages The multilanguage system operates based on a global translation list that contains all languages available in the installation. Languages can be [added to this list from the **Admin** panel](https://doc.ibexa.co/projects/userguide/en/6.0/content_management/translate_content/) in the back office. After adding a language be sure to dump all assets to the file system: ```bash yarn encore # OR php bin/console ibexa:encore:compile ``` **The new language must then be added to the [SiteAccess](https://doc.ibexa.co/en/saas/multisite/multisite/index.md) configuration**. Once this is done, any user with proper permissions can create content item versions in these languages in the user interface. ### Translatable and untranslatable fields Language versions consist of translated values of the content item's fields. In the content type definition every field is set to be Translatable or not. Cohesivo doesn't decide by itself which fields can be translated and which cannot. For some field values the need for a translation can be obvious, for example for the body of an article. In other cases, for instance images without text, integer numbers, or email addresses, translation is usually unnecessary. Despite that, Cohesivo gives you the possibility to mark any field as translatable regardless of its field type. It's only your decision to exclude the translation possibility for those fields where it makes no sense. When a field isn't flagged as Translatable, its value is copied from the initial/main translation when a new language version is created. This copied value cannot be modified. When a field is Translatable, you have to enter its value in a new language version manually. For example, let's say that you need to store information about marathon contestants and their results. You build a "contestant" content type that includes the following fields: name, photo, age, nationality, finish time. Allowing the translation of anything other than nationality would be pointless, since the values stored by the other fields are the same regardless of the language used to describe the contestant. In other words, the name, photo, age and finish time would be the same in, for example, both English and Norwegian. ### Access control You can control whether a user or user group is able to translate content or not. You do this by adding a [Language limitation](https://doc.ibexa.co/en/saas/permissions/limitation_reference/#language-limitation) to policies that allow creating or editing content. This limitation enables you to define which role can work with which languages in the system. For more information of the permissions system, see [Permissions](https://doc.ibexa.co/en/saas/permissions/permissions/index.md). In addition, you can also control the access to the global translation list by using the `Content/Translations` policy. This policy allows users to add and remove languages from the global translation list. ## Using SiteAccesses for handling translations If you want to have completely separate versions of the website, each with content in its own language, you can [use SiteAccesses](#using-siteaccesses-for-handling-translations). Depending on the URI used to access the website, a different site opens, with a language set in configuration settings. All content items are then displayed in this language. For details, see [Multi-language SiteAccesses](https://doc.ibexa.co/en/saas/multisite/set_up_translation_siteaccess/index.md). ### Explicit translation SiteAccesses Configuration isn't mandatory, but can help to distinguish which SiteAccesses can be considered translation SiteAccesses. ```yaml ibexa: siteaccess: default_siteaccess: eng list: - site - eng - fre - site_admin groups: frontend_group: - site - eng - fre # ... system: # Specifying which SiteAccesses are used for translation frontend_group: translation_siteaccesses: [fre, eng] eng: languages: [eng-GB] fre: languages: [fre-FR, eng-GB] site: languages: [eng-GB] ``` > **Note: Note** > > The top prioritized language is always used the SiteAccess language reference (for example, `fre-FR` for `fre` SiteAccess in the example above). If several translation SiteAccesses share the same language reference, **the first declared SiteAccess always applies**. #### Custom locale configuration If you need to use a custom locale, you can configure it in `ibexa.yaml`, adding it to the *conversion map*: ```yaml ibexa: # Locale conversion map between eZ Publish format (e.g. fre-FR) to POSIX (e.g. fr_FR). # The key is the eZ Publish locale. Check locale.yaml in IbexaCore to see natively supported locales. locale_conversion: eng-DE: en_DE ``` A locale *conversion map* example [can be found in `ibexa/core`, in `locale.yaml`](https://github.com/ibexa/core/blob/6.0/src/bundle/Core/Resources/config/locale.yml). ### More complex translation setup There are some cases where your SiteAccesses share settings (for example, repository or content settings), but you don't want all of them to share the same `translation_siteaccesses` setting. This can be for example the case when you use separate SiteAccesses for mobile versions of a website. The solution is defining new groups: ```yaml ibexa: siteaccess: default_siteaccess: eng list: - site - eng - fre - mobile_eng - mobile_fre - site_admin groups: # This group can be used for common front settings common_group: - site - eng - fre - mobile_eng - mobile_fre frontend_group: - site - eng - fre mobile_group: - mobile_eng - mobile_fre # ... system: # Translation SiteAccesses for regular frontend frontend_group: translation_siteaccesses: [fre, eng] # Translation SiteAccesses for mobile frontend mobile_group: translation_siteaccesses: [mobile_fre, mobile_eng] eng: languages: [eng-GB] fre: languages: [fre-FR, eng-GB] site: languages: [eng-GB] mobile_eng: languages: [eng-GB] mobile_fre: languages: [fre-FR, eng-GB] ``` ### Using implicit *related SiteAccesses* If the `translation_siteaccesses` setting isn't provided, implicit *related SiteAccesses* is used instead. SiteAccesses are considered *related* if they share: - The same repository - The same root `location_id` (see [Multisite](https://doc.ibexa.co/en/saas/multisite/multisite/index.md)) ### Fallback languages and missing translations When setting up SiteAccesses with different language versions, you can specify a list of preset languages for each SiteAccess. When this SiteAccess is used, the system goes through this list. If a content item is unavailable in the first (prioritized) language, it attempts to use the next language in the list, and more. Thanks to this you can have a fallback in case of a lacking translation. You can also assign a Default content availability flag to content types (available in the **Admin** panel). When this flag is assigned, content items of this type are available even when they don't have a language version in any of the languages configured for the current SiteAccess. If a language isn't provided in the list of prioritized languages and it's not the content item's first language, the URL alias for this content in this language isn't generated. # Language API > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). You can add languages to the system and get information about existing languages via the PHP API. You can manage languages configured in the system with PHP API by using [`LanguageService`](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-Core-Repository-LanguageService.html). ## Getting language information To get a list of all languages in the system use [`LanguageService::loadLanguages`:](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-Core-Repository-LanguageService.html#method_loadLanguage) ```php $languageList = $this->languageService->loadLanguages(); foreach ($languageList as $language) { $output->writeln($language->languageCode . ': ' . $language->name); } ``` ## Creating a language To create a new language, you need to create a [`LanguageCreateStruct`](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-Core-Repository-Values-Content-LanguageCreateStruct.html) and provide it with the language code and language name. Then, use [`LanguageService::createLanguage`](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-Core-Repository-LanguageService.html#method_createLanguage) and pass the `LanguageCreateStruct` to it: ```php $languageCreateStruct = $this->languageService->newLanguageCreateStruct(); $languageCreateStruct->languageCode = 'pol-PL'; $languageCreateStruct->name = 'Polish'; $this->languageService->createLanguage($languageCreateStruct); $output->writeln('Added language Polish with language code pol-PL.'); ``` # Back office translations > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). The language of the back office is selected automatically based on browser language, or you can choose it manually in user settings. ## Enabling back office languages All translations are available as a part of Cohesivo. To enable back office translations, use the following configuration: ```yaml ibexa: ui: translations: enabled: true ``` Then clear the cache. Now you can reload your Cohesivo back office. If your browser language is set to French, the back office is displayed in French. > **Tip: Checking browser language** > > To make sure that a language is set in your browser, check if it's sent as an accepted language in the `Accept-Language` header. > **Tip: Tip** > > You can also manually add the necessary .xliff files to an existing project. > > Add the language to an array under `ibexa.system..user_preferences.additional_translations`, for example: > > `ibexa.system..user_preferences.additional_translations: ['pl_PL', 'fr_FR']` > > Then, run `composer run post-update-cmd` and `php bin/console cache:clear --siteaccess=admin`. ### Selecting back office language Once you have language packages enabled, you can switch the language of the back office in the **User Settings** menu. Otherwise, the language is selected based on the browser language. If you don't have a language defined in the browser, the language is selected based on `parameters.locale_fallback` in `config/packages/ibexa.yaml`. ## Custom string translations When you extend the back office you often need to provide labels for new elements. It's good practice to provide your labels in translations files, instead of literally, so they can be reused and translated into other languages. To provide label strings, make use of the `Symfony\Component\Translation\TranslatorInterface` and its `trans()` method. The method takes as arguments: - `id` of the message you want to translate - an array of parameters - domain of the string Here's an example: ```php use Symfony\Contracts\Translation\TranslatorInterface; final readonly class MyService { public function __construct(private TranslatorInterface $translator) { } public function getTranslatedDescription(): string { return $this->translator->trans( 'custom.extension.description', [], 'custom_extension' ); } } ``` The strings are provided in .xliff files. The file should be stored in your project's or your bundle's `Resources/translations` folder. File name corresponds to the selected domain and the language, for example, `custom_extension.en.xliff`. ```xml
    The source node in most cases contains the sample message as written by the developer. If it looks like a dot-delimitted string such as "form.label.firstname", then the developer has not provided a default message.
    My custom label My custom label key: custom.extension.description
    ``` To provide a translation into another language, add it in the `` tag. For example, in `custom_extension.de.xliff`: ```xml My custom label Meine benutzerdefinierte Bezeichnung key: custom.extension.description ``` The language to display is then selected automatically based on [user preferences or browser setup](#selecting-back-office-language). > **Note: Note** > > Run `composer run post-update-cmd` which installs your JavaScript translations by using `BazingaJsTranslationBundle`, and clears the cache of the default SiteAccess. > > Run `php bin/console cache:clear --siteaccess=admin` to clear the back office cache. You may need to replace `admin` with the back office's SiteAccess name used in your installation. # Translations management > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). Translations management brings multiple features that help managers, developers and localization teams automate multilingual content delivery. Editions: LTS Update Translations management helps Cohesivo developers and editors deliver automated content item and product translations. - [Translations management product guide](https://ez-systems-developer-documentation--3381.com.readthedocs.build/en/3381/multisite/translations_management/translations_management_guide/): Translations management helps managers, developers and localization teams with multilingual content delivery. - [Configure translations management](https://ez-systems-developer-documentation--3381.com.readthedocs.build/en/3381/multisite/translations_management/configure_translations_management/): Install translations management and configure translation providers, language pairs, and more. - [Translate content items with CLI](https://ez-systems-developer-documentation--3381.com.readthedocs.build/en/3381/multisite/translations_management/translate_with_cli/): Use CLI command to translate content items. - [Extend translations management](https://ez-systems-developer-documentation--3381.com.readthedocs.build/en/3381/multisite/translations_management/extend_translations_management/): Add custom classes, exclude custom content types and add support for custom fields. - [Translations management events](https://ez-systems-developer-documentation--3381.com.readthedocs.build/en/3381/api/event_reference/translations_management_events/): Events that are triggered when working with translations management. - [PHP API Reference](https://ez-systems-developer-documentation--3381.com.readthedocs.build/en/3381/api/php_api/php_api_reference/namespaces/ibexa-contracts-translationsmanagement.html): Ibexa\\Contracts\\TranslationsManagement # Translations management product guide > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). Translations management helps managers, developers and localization teams with multilingual content delivery. Editions: LTS Update ## What is Translations management Content managers, editors, translators, and proofreaders who work with multilingual content in Cohesivo often face a common set of challenges: - context is lost when the source text isn't visible alongside the translation - translating long and complex content items is time-consuming - quality assurance is slow and error-prone without a direct comparison view - switching between tools or tabs to cross-reference languages disrupts focus and slows down publishing The Translations management package addresses these pain points through a side-by-side view, machine translation and the ability to invite reviewers to collaborate on the translation of content items or products. The package integrates with the [AI Actions framework](https://doc.ibexa.co/en/saas/ai/ai_actions/ai_actions_guide/index.md) to support machine translation providers such as Google Translate and DeepL, and AI-powered translation services like OpenAI, Anthropic, and Google Gemini. Administrators can manage providers and configure default provider-to-language-pair mappings directly in Cohesivo's back office, while editors can trigger machine translation from the content editing interface. ## Availability Translations management is an opt-in capability available as an [LTS Update](https://doc.ibexa.co/en/saas/ibexa_products/editions/#lts-updates) for all Cohesivo editions, starting with the v5.0.10 version. ## How it works Before the translation flow can happen, an administrator sets up the translation providers and assigns language pairs to them. Then, when an editor opens a content item or product and requests a new machine translation, the system resolves which provider to use. If no language-pair rule matches, it falls back to the user's manual selection. The system then extracts the translatable fields from the source language version of a content item and sends them to the configured provider's API. The system writes the translated strings into a target-language draft of the content item or a target-language version of a product, and opens it in a side-by-side view for the editor to review and refine. The editor can save the result of content item translation as a draft, share it with a reviewer or publish it. Product translations are published when the editor closes the view without rejecting it. ![Translations management flow for content item translation](https://doc.ibexa.co/en/saas/multisite/img/translations_management_flow.png "Translations management flow for content item translation") ## Capabilities ### Translation provider management Administrators can manage translation providers and configure translation provider/language combination assignments ([language pairs](https://doc.ibexa.co/en/saas/multisite/translations_management/configure_translations_management/#define-language-pairs)). This allows administrators to define which provider handles which language combination. Editors see the configured provider pre-selected when creating a new translation, but can override it if needed. ![Creating a language pair](https://doc.ibexa.co/en/saas/multisite/img/translations_management_language_pairs.png "Creating a language pair") The package provides integrations with several translation providers, including REST API-based services such as Google Translate and DeepL, and AI-powered services through the [AI Actions](https://doc.ibexa.co/en/saas/ai/ai_actions/ai_actions_guide/index.md). ### Side-by-side translation view Translations management introduces a [side-by-side translation view](https://doc.ibexa.co/projects/userguide/en/6.0/content_management/translate_content/#side-by-side-translation-view) that displays the read-only source language content next to an editable target language form. In this view, editors can provide and review translations in context, without having to leave the content editing interface. ![Side-by-side translation view](https://doc.ibexa.co/en/saas/multisite/img/managing_translations_sxs_view.png "Side-by-side translation view") Editors can: - access the side-by-side view when creating a new translation, reviewing an existing one, or editing a draft - compare source and target content field by field while editing - copy all content from the source column to the target column with a single action - provide localized versions of media assets and their alternative text - use the distraction-free mode for focused editing of individual fields, with AI actions available inline - choose whether the source column appears on the left or right in user settings > **Note: Excluded content types** > > Content types that are editable in [Page builder](https://doc.ibexa.co/en/saas/content_management/pages/page_builder_guide/index.md) or [Form builder](https://doc.ibexa.co/en/saas/content_management/forms/form_builder_guide/index.md) are excluded from side-by-side editing. > > Products are editable in the side-by-side view, but [product attributes aren't translatable](https://doc.ibexa.co/en/saas/product_catalog/products/#product-attributes). ### Command-line translation The Translations management package exposes a [console command](https://doc.ibexa.co/en/saas/multisite/translations_management/translate_with_cli/index.md) for translating content items from the command line. You can use it for batch processing or automated workflows. ### Translation review When a draft translation of a content item or product is created by going through the automatic translation process in the back office, the system creates a review status record and marks the draft as "For review". The console command bypasses this and drafts created with command-line translation aren't assigned a review status. Editors can [accept or reject the translation](https://doc.ibexa.co/projects/userguide/en/6.0/content_management/translate_content/#review-automatic-translation) directly in the side-by-side view. Accepted drafts are marked as "Translated". When the editor rejects the translation, the status doesn't change, but the system records that the draft translation required corrections for statistical purposes. A draft translation in the "Translated" state can't be rejected anymore. The `ibexa_auto_translation_review` workflow is separate from the [editorial workflow](https://doc.ibexa.co/en/saas/content_management/workflow/workflow/index.md). Accepting or rejecting draft translations does not trigger editorial workflow transitions or notifications. > **Note: No review for human translations** > > Draft translations that were created by a human don't have a review status. ### Extensibility Developers can [extend the translations management](https://doc.ibexa.co/en/saas/multisite/translations_management/extend_translations_management/index.md) package: - create custom translation providers - add support for custom fields - add custom content type exclusion rules - tap into the translation lifecycle with [events](https://doc.ibexa.co/en/saas/api/event_reference/translations_management_events/index.md) ## Benefits ### Streamlined translation process Translations management reduces the time needed to create and publish multilingual content. Editors can initiate machine translation directly from the content editing interface and work on the result immediately in the side-by-side translation view, without having to switch contexts or use another translation tool. ### Better translation quality and consistency Machine-translated drafts are marked for review, allowing editors to accept or reject them directly in the side-by-side translation view. This eliminates the need for a separate workflow or tool. With the side-by-side translation view, editors can conveniently compare source and target content while editing. Seeing the translation in context makes it easier to identify omissions, inconsistencies, and translation errors. ### Flexible support for different translation providers Regardless of technical and conceptual differences, the experience of working with various translation providers is the same. Administrators can assign providers to specific language pairs and editors can override the assignment when needed. ### Readiness for automated processing The CLI command enables integration with automated processes, which can help you reduce manual effort for large content volumes. # Configure translations management > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). Install translations management and configure translation providers, language pairs, and more. Editions: LTS Update `ibexa/translations-management` extends Cohesivo's built-in language management tools that editors use for content item and product translation. It introduces a plugin that handles automatic translations through the translation provider system by connecting to REST APIs and AI services. By using the new [side-by-side editing interface](#side-by-side-translation-view), editors can compare source and target values, provide content item and product translations in a single view, and reject or approve translations. There are multiple extension points that you can use to [customize different areas of the translation workflow](https://doc.ibexa.co/en/saas/multisite/translations_management/extend_translations_management/index.md). > **Note: Translation limitations** > > The following limitations apply to automatic translation: > > - Content types that contain the `ibexa_form` or `ibexa_landing_page` fields don't support the side-by-side translation view and open in the single-language editor instead. > - For `ibexa_landing_page` fields, translatable attributes of block content are sent to the translation provider, while layout, zones, and non-translatable block attributes are preserved. > - The value of `ibexa_form` field type is not translated. > > Also, [product attributes](https://doc.ibexa.co/en/saas/product_catalog/products/#product-attributes) remain non-translatable and are inactive in the side-by-side translation view. ## Install package To install the Translations management [LTS Update](https://doc.ibexa.co/en/saas/ibexa_products/editions/#lts-updates), run the following command: ```bash composer require ibexa/translations-management ``` If you're installing Translations management LTS Update as part of the installation process of a fresh Cohesivo instance, this step copies the migration files into the project's migrations directory. It also creates the database tables required for the review workflow, and adds the default action configurations in the database. Otherwise follow the steps below. ### Existing installations To add the Translations management LTS Update to an existing Cohesivo instance, after installation, you must create database tables and action configurations yourself. #### Modify database schema Add the tables needed by the bundle: **MySQL** ```sql CREATE TABLE IF NOT EXISTS ibexa_auto_translation ( id INT AUTO_INCREMENT NOT NULL, provider_identifier VARCHAR(190) NOT NULL, content_id INT NOT NULL, version_no INT NOT NULL, source_language_id BIGINT NOT NULL, target_language_id BIGINT NOT NULL, review_status VARCHAR(64) NOT NULL, created_at DATETIME NOT NULL COMMENT '(DC2Type:datetime_immutable)', updated_at DATETIME NOT NULL COMMENT '(DC2Type:datetime_immutable)', INDEX ibexa_auto_translation_content_version_idx (content_id, version_no), INDEX ibexa_auto_translation_target_language_idx (target_language_id), INDEX ibexa_auto_translation_review_status_idx (review_status), UNIQUE INDEX ibexa_auto_translation_context_uidx (content_id, version_no, source_language_id, target_language_id), PRIMARY KEY(id) ) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_520_ci` ENGINE = InnoDB; CREATE TABLE IF NOT EXISTS ibexa_auto_translation_review_log ( id INT AUTO_INCREMENT NOT NULL, auto_translation_id INT DEFAULT NULL, user_id INT NOT NULL, status VARCHAR(64) NOT NULL, operation VARCHAR(64) NOT NULL, created_at DATETIME NOT NULL COMMENT '(DC2Type:datetime_immutable)', INDEX IDX_325A3B737CE350E8 (auto_translation_id), INDEX ibexa_auto_translation_review_log_auto_translation_created_idx (auto_translation_id, created_at, id), INDEX ibexa_auto_translation_review_log_status_created_idx (status, created_at), INDEX ibexa_auto_translation_review_log_user_idx (user_id), PRIMARY KEY(id) ) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_520_ci` ENGINE = InnoDB; ALTER TABLE ibexa_auto_translation_review_log ADD CONSTRAINT ibexa_auto_translation_review_log_auto_translation_fk FOREIGN KEY (auto_translation_id) REFERENCES ibexa_auto_translation (id) ON UPDATE CASCADE ON DELETE SET NULL; ALTER TABLE ibexa_auto_translation_review_log ADD CONSTRAINT ibexa_auto_translation_review_log_user_fk FOREIGN KEY (user_id) REFERENCES ibexa_user (contentobject_id) ON UPDATE CASCADE ON DELETE RESTRICT; ``` **PostgreSQL** ```sql CREATE TABLE IF NOT EXISTS ibexa_auto_translation ( id SERIAL NOT NULL, provider_identifier VARCHAR(190) NOT NULL, content_id INT NOT NULL, version_no INT NOT NULL, source_language_id BIGINT NOT NULL, target_language_id BIGINT NOT NULL, review_status VARCHAR(64) NOT NULL, created_at TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL, updated_at TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL, PRIMARY KEY(id) ); CREATE INDEX IF NOT EXISTS ibexa_auto_translation_content_version_idx ON ibexa_auto_translation (content_id, version_no); CREATE INDEX IF NOT EXISTS ibexa_auto_translation_target_language_idx ON ibexa_auto_translation (target_language_id); CREATE INDEX IF NOT EXISTS ibexa_auto_translation_review_status_idx ON ibexa_auto_translation (review_status); CREATE UNIQUE INDEX IF NOT EXISTS ibexa_auto_translation_context_uidx ON ibexa_auto_translation (content_id, version_no, source_language_id, target_language_id); COMMENT ON COLUMN ibexa_auto_translation.created_at IS '(DC2Type:datetime_immutable)'; COMMENT ON COLUMN ibexa_auto_translation.updated_at IS '(DC2Type:datetime_immutable)'; CREATE TABLE IF NOT EXISTS ibexa_auto_translation_review_log ( id SERIAL NOT NULL, auto_translation_id INT DEFAULT NULL, user_id INT NOT NULL, status VARCHAR(64) NOT NULL, operation VARCHAR(64) NOT NULL, created_at TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL, PRIMARY KEY(id) ); CREATE INDEX IF NOT EXISTS IDX_325A3B737CE350E8 ON ibexa_auto_translation_review_log (auto_translation_id); CREATE INDEX IF NOT EXISTS ibexa_auto_translation_review_log_auto_translation_created_idx ON ibexa_auto_translation_review_log (auto_translation_id, created_at, id); CREATE INDEX IF NOT EXISTS ibexa_auto_translation_review_log_status_created_idx ON ibexa_auto_translation_review_log (status, created_at); CREATE INDEX IF NOT EXISTS ibexa_auto_translation_review_log_user_idx ON ibexa_auto_translation_review_log (user_id); COMMENT ON COLUMN ibexa_auto_translation_review_log.created_at IS '(DC2Type:datetime_immutable)'; ALTER TABLE ibexa_auto_translation_review_log ADD CONSTRAINT ibexa_auto_translation_review_log_auto_translation_fk FOREIGN KEY (auto_translation_id) REFERENCES ibexa_auto_translation (id) ON UPDATE CASCADE ON DELETE SET NULL; ALTER TABLE ibexa_auto_translation_review_log ADD CONSTRAINT ibexa_auto_translation_review_log_user_fk FOREIGN KEY (user_id) REFERENCES ibexa_user (contentobject_id) ON UPDATE CASCADE ON DELETE RESTRICT; ``` The script creates the required data structures, but doesn't add any data to the database. #### Add action configurations To complete the setup, import and run the AI Action Configuration migrations required by the [AI connectors](https://doc.ibexa.co/en/saas/ai/ai_actions/configure_ai_actions/index.md) that you use: ```bash php bin/console ibexa:migrations:import vendor/ibexa/translations-management/src/bundle/Resources/migrations/2026_05_06_15_00_auto_translate_openai_action_configuration.yaml php bin/console ibexa:migrations:import vendor/ibexa/translations-management/src/bundle/Resources/migrations/2026_05_11_10_00_auto_translate_gemini_action_configuration.yaml php bin/console ibexa:migrations:import vendor/ibexa/translations-management/src/bundle/Resources/migrations/2026_05_12_08_30_auto_translate_anthropic_action_configuration.yaml php bin/console ibexa:migrations:migrate ``` ## Configure translation providers Translation providers are the services that perform the actual text translation. If you fail to configure them, the automatic translation feature is disabled in the editor's UI, and a message is displayed that prompts the user to contact the administrator. The Translations management package comes with two types of translation services: - **REST API-based providers** - call a translation service such as Google Translate or DeepL directly by using an API key. - **AI-based providers** - send translation requests through the [AI Actions](https://doc.ibexa.co/en/saas/ai/ai_actions/configure_ai_actions/index.md) framework, relying on the same model selection and policy controls as other AI features in Cohesivo. > **Note: Prerequisites for the default translation providers** > > Before you can configure translation providers, you must meet the following prerequisites: > > - For the REST API-based translation providers, add API keys that you obtain from the machine translation services to the `.env` file in the root directory of your project. > - For the AI-based translation providers, [configure AI Actions and the corresponding connectors](https://doc.ibexa.co/en/saas/ai/ai_actions/configure_ai_actions/index.md). Out of the box, Translations management can support the following translation providers: | Provider | Type | | ------------------ | ---------- | | Google Translate | REST API | | DeepL | REST API | | OpenAI | AI Actions | | Anthropic (Claude) | AI Actions | | Google Gemini | AI Actions | ### Built-in AI providers If you meet the above prerequisites, and you install the Translations management package, the installation process automatically creates AI [Action Configurations](https://doc.ibexa.co/en/saas/ai/ai_actions/extend_ai_actions/#action-configurations) for OpenAI (`auto_translate_openai`), Google Gemini (`auto_translate_gemini`), and Anthropic Claude (`auto_translate_anthropic`). You can use them directly in provider configuration: | Action Configuration identifier | Handler | Default model | | ------------------------------- | ------------------------ | -------------------------- | | `auto_translate_openai` | `openai-text-to-text` | `gpt-5` | | `auto_translate_gemini` | `gemini-text-to-text` | `gemini-pro-latest` | | `auto_translate_anthropic` | `anthropic-text-to-text` | `claude-sonnet-4-20250514` | You can then [customize these configurations in the UI](https://doc.ibexa.co/projects/userguide/en/6.0/ai_actions/work_with_ai_actions/#edit-existing-ai-actions). ### Add YAML configuration In `config/packages`, create a `translations_management.yaml` file. You configure the providers in the SiteAccess-aware `translations_management` namespace. ```yaml ibexa: system: default: translations_management: auto_translate: providers: google: apiKey: '%env(GOOGLE_TRANSLATE_API_KEY)%' deepl: apiKey: '%env(DEEPL_API_KEY)%' openai: actionConfigurationIdentifier: 'auto_translate_openai' anthropic: actionConfigurationIdentifier: 'auto_translate_anthropic' gemini: actionConfigurationIdentifier: 'auto_translate_gemini' ``` The `apiKey` values must reference API key values that you added to the `.env` file. The `actionConfigurationIdentifier` values must reference existing Action Configurations. If a value is missing or empty, the provider doesn't appear in the UI as a selectable option. #### Advanced translation provider options In addition to their required authentication keys, all providers support two optional ones: - `supportedLanguageCodes` - overrides the default list of language codes that this provider accepts - `languageCodesMap` - maps language codes used by Cohesivo, for example, `eng-GB`, to the provider-specific codes the API expects REST API-based providers come with their own language code lists and mappings, therefore both settings are optional. If configured, they replace the built-in defaults, so use them to restrict available languages or override mappings. > **Tip: Default values** > > To check the built-in defaults for the existing providers, run: > > ```bash > php bin/console debug:container --parameters | grep ibexa.translations_management.auto_translate.provider > ``` > > The output lists the default `supported_language_codes` and `language_codes_map` values for each configured provider, which you can use as a reference. AI-based providers don't provide built-in language code lists or mappings. If `supportedLanguageCodes` is not configured, all enabled languages are used, converted to POSIX format. If `languageCodesMap` is not configured, the system automatically tries to match Cohesivo language codes to the one supported by the provider by trying different format variants, for example, `eng-GB`, `en-GB`, or `en`. If no match is found, an `UnsupportedLanguageException` is thrown at runtime. Therefore, for AI-based providers, it's recommended that you explicitly configure both options. ```yaml ibexa: system: default: translations_management: auto_translate: providers: # ... openai: actionConfigurationIdentifier: 'auto_translate_openai' supportedLanguageCodes: - 'eng-GB' - 'ger-DE' - 'fre-FR' languageCodesMap: eng-GB: 'en' ger-DE: 'de' fre-FR: 'fr' ``` The `supportedLanguageCodes` setting controls which languages are available when creating [language pairs](#define-language-pairs) for this provider. > **Note: Identifier normalization** > > Provider identifiers are normalized from hyphens to underscores during configuration processing. Use one format consistently. If you mix `my-provider` and `my_provider` for the same provider, it results in an exception. ## Define language pairs Language pair definitions decide which provider handles each source-to-target language combination by default. For example, you can decide that English to French translations should use DeepL. When an editor [opens the translation modal](https://doc.ibexa.co/projects/userguide/en/6.0/content_management/translate_content/#add-new-translation) and selects a matching language combination, the provider that you chose is pre-selected in the dropdown. The editor can override the pre-selection. The list of languages available when creating a language pair is determined by what each provider supports. You can only select the languages that are present in a provider's [supported list](#advanced-translation-provider-options) for that provider's pairs. You [manage language pairs in the back office](https://doc.ibexa.co/projects/userguide/en/6.0/content_management/translate_content/#manage-translation-services-and-language-pairs). ## Side-by-side translation view The [side-by-side translation view](https://doc.ibexa.co/projects/userguide/en/6.0/content_management/translate_content/#side-by-side-translation-view) is a two-column content editing interface where the source column is read-only and the target column is an editable form. Content types that contain the `ibexa_landing_page` or `ibexa_form` fields can't be opened in the side-by-side translation view. Editors can open them in the standard single-language editor. You can exclude the support for additional content types if needed. To do it, [define custom exclusion rules](https://doc.ibexa.co/en/saas/multisite/translations_management/extend_translations_management/#define-custom-exclusion-rules). > **Note: Meta fields** > > Fields marked with [`meta: true`](https://doc.ibexa.co/en/saas/administration/back_office/content_tab_switcher/#add-meta-tab) and fields that belong to groups listed in [`admin_ui_forms.content_edit.meta_field_groups_list`](https://doc.ibexa.co/en/saas/administration/back_office/content_tab_switcher/#configure-field-groups-for-meta-tab) aren't rendered in the side-by-side translation view. For a description of the side-by-side view and its functions from the editor's perspective, see [User Documentation](https://doc.ibexa.co/projects/userguide/en/6.0/content_management/translate_content/#side-by-side-translation-view). ### User settings The Translations management package adds preferences that editors can configure under their [user settings](https://doc.ibexa.co/projects/userguide/en/6.0/getting_started/get_started/#user-settings). Each editor can configure them independently, and they don't affect other users. For example, editors can choose whether the target language column appears on the left or right in the side-by-side translation view. By default, the target is on the right, and each editor can override this default. You can change the system-wide default in configuration: ```yaml parameters: ibexa.site_access.config.default.translations_management.default_side_by_side_column_order: source_right_target_left ``` The accepted values are `source_left_target_right` (default) and `source_right_target_left`. # Translate content items with CLI > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). Use CLI command to translate content items. Editions: LTS Update For the purposes of batch processing, automation, and other scripted actions, the [Translations management](https://doc.ibexa.co/en/saas/multisite/translations_management/translations_management_guide/index.md) package exposes a command that automatically translates content items or products by using any of the configured providers: ```bash php bin/console ibexa:translations:auto-translate-content \ --content-id=42 \ --provider=deepl \ --from=eng-GB \ --to=fre-FR ``` > **Tip: Command alias** > > You can use `ibexa:translations:translate-content` as an alias. The command uses the same provider configuration and field value transformers as the UI. Therefore, depending on the specific command options used, the result can be the same as if an editor [triggered the automated translation manually](https://doc.ibexa.co/projects/userguide/en/6.0/content_management/translate_content/#add-new-translation). Without the `--draft-only` option, the translation generated with a CLI command is instantly published, while a manual one requires that a human publishes it. ## CLI command options | Option | Required | Description | | -------------- | -------- | ------------------------------------------------------------------------------------------ | | `--content-id` | Yes | ID of the content item or product to translate | | `--provider` | Yes | Identifier of the translation provider to use | | `--from` | Yes | Source language code | | `--to` | Yes | Target language code | | `--user-id` | No | Repository user ID to run the translation (default: `14`, which is the Administrator user) | | `--draft-only` | No | Create a translated draft without publishing it | # Extend translations management > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). Add custom classes, exclude custom content types and add support for custom fields. Editions: LTS Update By extending [Translations management](https://doc.ibexa.co/en/saas/multisite/translations_management/translations_management_guide/index.md), you can adapt the package's behavior to your specific requirements. The package is designed to be extended in multiple ways. You can create custom [translation providers](https://doc.ibexa.co/en/saas/multisite/translations_management/configure_translations_management/#configure-translation-providers), field type transformers, exclusion rules, and UI components. In all cases, you follow the same pattern: implement an interface first, then register the service with a service tag. The package discovers and registers tagged services automatically. ## Add custom translation provider Before you build a custom translation provider, if your provider uses the AI Actions framework, make sure that the `ibexa/connector-ai` package is configured in your system. ### REST API-based provider To connect a translation service that calls a REST API directly, implement [`TranslationProviderInterface`](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-TranslationsManagement-AutoTranslate-Provider-TranslationProviderInterface.html). For providers that store API keys and other required settings, you can rely on [`ConfigurableProviderInterface`](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-TranslationsManagement-AutoTranslate-Provider-ConfigurableProviderInterface.html). It extends `TranslationProviderInterface` and adds `getConfiguration()` and `isConfigured()` methods. The `translate()` method receives a [`TranslationDataInterface`](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-TranslationsManagement-AutoTranslate-TranslationDataInterface.html) object that carries the text to translate along with the source and target [language codes](https://doc.ibexa.co/en/saas/multisite/translations_management/configure_translations_management/#advanced-translation-provider-options): ```php apiClient->translate( $translationData->getText(), $translationData->getSourceLanguage(), $translationData->getTargetLanguage() ); } /** @return array */ public function getSupportedLanguageCodes(): array { return ['eng-GB', 'ger-DE', 'fre-FR']; } } ``` Register the provider with the `ibexa.translations_management.auto_translate.provider` tag. Both `identifier` and [`validation_profile`](#validation-profiles) attributes are required. ```yaml services: App\TranslationsManagement\MyCustomProvider: tags: - name: 'ibexa.translations_management.auto_translate.provider' identifier: 'my_custom_provider' validation_profile: 'my_custom_profile' ``` ### AI-based provider To connect a translation service that uses the [AI Actions](https://doc.ibexa.co/en/saas/ai/ai_actions/ai_actions/index.md) framework, implement [`AiTranslationProviderInterface`](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-TranslationsManagement-AutoTranslate-Provider-AiTranslationProviderInterface.html). This interface extends `ConfigurableProviderInterface` and serves as a type marker for AI-based providers. The system uses the `getConfiguration()` and `isConfigured()` methods to determine whether the provider is available before displaying selectable options in the **Create a new translation** modal: ```php apiClient->translate( $translationData->getText(), $translationData->getSourceLanguage(), $translationData->getTargetLanguage() ); } /** @return array */ public function getSupportedLanguageCodes(): array { return ['eng-GB', 'ger-DE', 'fre-FR']; } /** @return array */ public function getConfiguration(): array { return [ 'actionConfigurationIdentifier' => $this->actionConfigurationIdentifier, ]; } public function isConfigured(): bool { return $this->actionConfigurationIdentifier !== ''; } } ``` Register the provider with the `ibexa.translations_management.auto_translate.provider` tag, with `ai_generic` as the validation profile. The `ai_generic` validation profile is meant to be used by default for AI providers, but you can [implement your own](#validation-profiles). ```yaml services: App\TranslationsManagement\MyCustomAiProvider: tags: - name: 'ibexa.translations_management.auto_translate.provider' identifier: 'my_custom_ai_provider' validation_profile: 'ai_generic' ``` If your custom provider integrates with the AI Actions framework, `isConfigured()` should check whether the `actionConfigurationIdentifier` resolves to an existing and enabled Action Configuration. The `validation_profile`, `supportedLanguageCodes`, and `languageCodesMap` options work the same way as for REST API-based providers. ### Language code normalizer If your provider uses [language codes](https://doc.ibexa.co/en/saas/multisite/translations_management/configure_translations_management/#advanced-translation-provider-options) that differ from the ones used by Cohesivo and the `languageCodesMap` configuration is insufficient, implement a custom [`LanguageNormalizerInterface`](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-TranslationsManagement-AutoTranslate-Provider-LanguageNormalizer-LanguageNormalizerInterface.html) to handle the conversion: ```php 'en-GB', 'ger-DE' => 'de', 'fre-FR' => 'fr', ]; public function supports(TranslationProviderInterface $provider): bool { return $provider->getIdentifier() === 'my_custom_ai_provider'; } public function normalize( TranslationProviderInterface $provider, string $languageCode ): string { if (isset(self::LANGUAGE_MAP[$languageCode])) { return self::LANGUAGE_MAP[$languageCode]; } throw new UnsupportedLanguageException( $languageCode, $provider->getIdentifier(), array_values(self::LANGUAGE_MAP) ); } } ``` The `supports()` method is a way to bind the normalizer to a provider. When a translation is triggered, the system checks the registered normalizers, and it uses the first one whose `supports()` method returns `true` for the current provider. Register the normalizer with the `ibexa.translations_management.auto_translate.provider.language_normalizer` tag: ```yaml services: App\TranslationsManagement\MyCustomLanguageNormalizer: tags: - name: 'ibexa.translations_management.auto_translate.provider.language_normalizer' priority: 10 ``` If multiple normalizers are registered, use `priority` to control the order in which they're checked. ### Validation profiles The `validation_profile` attribute links the provider to a validator that checks language codes and payload size before each translation request. By default, three profiles are available: | Profile | Used by | | ------------ | ------------------------------------------------------------ | | `google` | Google Translate provider | | `deepl` | DeepL provider | | `ai_generic` | All built-in AI providers. Suitable for custom AI providers. | To define a custom validation profile, implement [`ProviderValidatorInterface`](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-TranslationsManagement-AutoTranslate-Validator-ProviderValidatorInterface.html) and register it: ```yaml services: App\TranslationsManagement\MyProviderValidator: tags: - name: 'ibexa.translations_management.auto_translate.provider.validator' profile: 'my_custom_profile' ``` You can reuse the [`DefaultProviderValidator`](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-TranslationsManagement-AutoTranslate-Validator-DefaultProviderValidator.html) class if it meets your requirements or implement your own. It exposes configurable maximum payload size and language code regex patterns. ## Add support for custom field types The translation engine works by extracting translatable text from fields, sending it to the provider, and writing the translated text back. Field value transformers handle this encode/decode cycle, one per field type. The package includes transformers for `text`, `RichText`, and `ibexa_landing_page` fields. To add support for a custom or non-standard field type, implement [`FieldValueTransformerInterface`](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-TranslationsManagement-AutoTranslate-Transformer-Field-FieldValueTransformerInterface.html): - `getFieldTypeIdentifier()` - returns the field type identifier that this transformer handles - `encode(Field $field): EncodedFieldValue` - extracts the translatable string from the field and wraps it in an [`EncodedFieldValue`](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-TranslationsManagement-AutoTranslate-Transformer-Field-EncodedFieldValue.html). The constructor takes the extracted string as its first argument and an optional metadata array as the second. - `decode(string $value, mixed $previousFieldValue, array $metadata): Value` - receives the translated string, the previous field value, and any metadata. Returns the updated field value. The following example adds support for automatically translating the alternative text of an image: ```php getValue(); if (!$value instanceof ImageValue) { throw new InvalidArgumentException( '$field', sprintf('Expected %s, got %s.', ImageValue::class, get_debug_type($value)) ); } return new EncodedFieldValue($value->alternativeText ?? ''); } /** * @param array $metadata */ public function decode(string $value, mixed $previousFieldValue, array $metadata): Value { if (!$previousFieldValue instanceof ImageValue) { throw new InvalidArgumentException( '$previousFieldValue', sprintf('Expected %s, got %s.', ImageValue::class, get_debug_type($previousFieldValue)) ); } return new ImageValue([ 'id' => $previousFieldValue->id, 'fileName' => $previousFieldValue->fileName, 'fileSize' => $previousFieldValue->fileSize, 'uri' => $previousFieldValue->uri, 'imageId' => $previousFieldValue->imageId, 'inputUri' => $previousFieldValue->inputUri, 'width' => $previousFieldValue->width, 'height' => $previousFieldValue->height, 'alternativeText' => $value, 'additionalData' => $previousFieldValue->additionalData, 'mime' => $previousFieldValue->mime, ]); } } ``` Register the new transformer with the `ibexa.translations_management.auto_translate.field_value_transformer` tag. The `field_type_identifier` attribute is required. It must match the value that `getFieldTypeIdentifier()` returns: ```yaml services: App\TranslationsManagement\ImageAltTextTransformer: tags: - name: 'ibexa.translations_management.auto_translate.field_value_transformer' field_type_identifier: 'ibexa_image' ``` > **Note: Advanced metadata handling** > > When metadata is required for decoding or when you need to control what happens if metadata encoding fails, implement [`MetadataAwareFieldValueTransformerInterface`](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-TranslationsManagement-AutoTranslate-Transformer-Field-MetadataAwareFieldValueTransformerInterface.html). With this interface, you can fail the translation when metadata encoding fails and indicate that metadata is required for decoding. Without it, the field is skipped instead. ## Define custom exclusion rules Use exclusion rules to identify content that cannot use the side-by-side view. The Translations management package ships with one rule that excludes content types that contain `ibexa_landing_page` or `ibexa_form` fields. ### Exclude with custom class To exclude content from side-by-side view, for example, content types whose fields render incorrectly in the side-by-side layout, implement [`SideBySideExclusionRuleInterface`](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-TranslationsManagement-SideBySide-Service-SideBySideExclusionRuleInterface.html). The `isExcluded()` method receives a [`ContentInfo`](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-Core-Repository-Values-Content-ContentInfo.html) object, which gives you access to different criteria, including content type, section, owner, main language, publication status, visibility, and main location of the content item. If the content item should be excluded, the method should return `true`. ```php getContentType()->identifier === 'my_excluded_type'; } } ``` Register the rule with the `ibexa.translations_management.side_by_side.exclusion_rule` tag. This interface is not registered for [Symfony autoconfiguration](https://symfony.com/doc/7.4/service_container.html#the-autoconfigure-option), so the tag is required. ```yaml services: App\TranslationsManagement\MyCustomExclusionRule: tags: - { name: 'ibexa.translations_management.side_by_side.exclusion_rule' } app.translations_management.exclusion_rule.custom_field_types: ``` # Permissions # Permissions > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). Use granular permission system to grant access to various parts of the system by using roles, policies, and limitations. The permission system of Cohesivo enables you to control in detail which users have access to which parts of the system, both the back office's administrative and editorial features, and the content of the website front. - [Permission overview](https://ez-systems-developer-documentation--3381.com.readthedocs.build/en/3381/permissions/permission_overview/): The permission system is based on policies that you assign to users or user groups in the form of roles. - [Policies](https://ez-systems-developer-documentation--3381.com.readthedocs.build/en/3381/permissions/policies/): Policies are the main building block of the permissions system which lets you define the accesses for specific user roles. - [Permission use cases](https://ez-systems-developer-documentation--3381.com.readthedocs.build/en/3381/permissions/permission_use_cases/): Set up permission sets for common use cases. - [Limitations](https://ez-systems-developer-documentation--3381.com.readthedocs.build/en/3381/permissions/limitations/): Control access to parts of the system by fine-tuning permissions with the use of Limitations. # Permission overview > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). The permission system is based on policies that you assign to users or user groups in the form of roles. A new user doesn't have permissions for any part of the system, unless they're explicitly given access. To get access they need to inherit roles, typically assigned to the user group they belong to. Each role can contain one or more **Policies**. A policy is a rule that gives access to a single **function** in a **module**. For example, a `section/assign` policy allows the user to assign content to sections. When you add a policy to a role, you can also restrict it using one or more **Limitations**. A policy with a limitation only applies when the condition in the limitation is fulfilled. For example, a `content/publish` policy with a `ContentType` limitation on the "Blog Post" content type allows the user to publish only Blog Posts, and not other content. A limitation, like a policy, specifies what a user *can* do, not what they *can't do*. A `Section` limitation, for example, *gives* the user access to the selected section, not *prohibits* it. For more information, see [Limitation reference](https://doc.ibexa.co/en/saas/permissions/limitation_reference/index.md) and [Permission use cases](https://doc.ibexa.co/en/saas/permissions/permission_use_cases/index.md). ## Assigning roles to users Every user or user group can have many roles. A user can also belong to many groups, for example, Administrators, Editors, Subscribers. It's best practice to avoid assigning roles to users directly. Instead, try to organize your content so that it can be covered with general roles assigned to user groups. Using groups is easier to manage and more secure. It also improves system performance. The more role assignments and complex policies you add for a given user, the more complex the search/load queries are, because they always take permissions into account. ## Permissions for custom controllers You can control access to a custom controller by implementing the `performAccessCheck()` method. In the following example the user doesn't have access to the controller unless they have the `section/view` policy: ```php use Ibexa\Core\MVC\Symfony\Security\Authorization\Attribute; public function performAccessCheck(): void { parent::performAccessCheck(); $this->denyAccessUnlessGranted(new Attribute('section', 'view')); } ``` `Attribute` accepts three arguments: - `module` is the policy module (for example,`content`) - `function` is the function inside the module (for example, `read`) - `limitations` are optional limitations to check against. Here you can provide two keys: - `valueObject` is the object you want to check for, for example `ContentInfo`. - `targets` are a table of value objects that are the target of the operation. For example, to check if content can be assigned to a Section, provide the Section as `targets`. `targets` accept location, object state and section objects. ### Checking user access To check if a user has access to an operation, use the `isGranted()` method. For example, to check if content can be assigned to a Section: ```php $hasAccess = $this->isGranted( new Attribute('section', 'assign', ['valueObject' => $contentInfo, 'targets' => [$section]]) ); ``` You can also use the permission resolver (`Ibexa\Core\Repository\Permission\PermissionResolver`). The `canUser()` method checks if the user can perform a given action with the selected object. For example: `canUser('content', 'edit', $content, [$location] );` checks the `content/edit` permission for the provided content item at the provided location. ### Blocking access to controller action To block access to a specific action of the controller, add the following to the action's definition: ```php $this->denyAccessUnlessGranted(new Attribute('state', 'administrate')); ``` # Permission use cases > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). Set up permission sets for common use cases. Here are a few examples of sets of policies that you can use to get some common permission configurations. ## Enter back office To allow the user to enter the back office interface and view all content, set the following policies: - `user/login` - `content/read` - `content/versionread` - `section/view` - `content/reverserelatedlist` These policies are necessary for all other cases below that require access to the content structure. ## Create content without publishing (Experience) You can use this option together with Ibexa Experience's content review options. Users assigned with these policies can create content, but cannot publish it. To publish, they must send the content for review to another User with proper permissions (for example, senior editor or proofreader). - `content/create` - `content/edit` Use this setup with Ibexa Experience or Ibexa Commerce only, as Ibexa Headless doesn't allow the User to continue working with their content. ## Create and publish content To create and publish content, users must additionally have the following policies: - `content/create` - `content/edit` - `content/publish` This also lets the user copy and move content, and add new locations to a content item (but not remove them). ## Move content To move a content item or a subtree to another location, the user must have the following policies: - `content/read` - on the source location - `content/create` - on the target location ## Remove content To send content to Trash, the user needs to have the `content/remove` policy. If content has more than one language, the user must have access to all the languages. That is, the `content/remove` policy must have either no limitation, or a limitation for all languages of the content item. To remove an archived version of content, the user must have the `content/versionremove` policy. Further manipulation of Trash requires the `content/restore` policy to restore items from Trash, and `content/cleantrash` to completely delete all content from the Trash. > **Caution: Caution** > > With the `content/cleantrash` policy, the user can empty the Trash even if they don't have access to the trashed content, for example, because it belonged to a Section that the user doesn't have permissions for. ## Restrict editing to part of the tree If you want to let the User create or edit content, but only in one part of the content tree, use limitations. Three limitations that you could use here are `Section` limitation, `Location` limitation and `Subtree of Location` limitation. ### Section limitation Let's assume you have two Folders under your Home: Blog and Articles. You can let a user create content for the blogs, but not in Articles, by adding a `Section` limitation to the Blog content item. This allows the User to publish content anywhere under this location in the structure. Section doesn't have to belong to the same subtree of location in the content structure, any locations can be assigned to it. ### Location limitation If you add a `Location` limitation and point to the same location, the user is able to publish content directly under the selected location, but not anywhere deeper in its subtree of location. ### Subtree of location limitation To limit the user's access to a subtree, use the `Subtree of Location` limitation. You do it by creating two new roles for a user group: 1. Role with a `Subtree` limitation for the User 2. Role with a `Location` limitation for the subtree Follow the example below to learn how to do that. **Cookbook**, **Dinner recipes** and **Dessert recipes** containers aren't accessible in the frontend. Edit access to them in the **Admin** panel. ![Subtree file structure](https://doc.ibexa.co/en/saas/permissions/img/subtree_usability_notes_1.png) To give the vegetarian editors access only to the **Vegetarian** dinner recipes section, create a new role, for example, *EditorVeg*. Next, add to it a `content/read` policy with the `Subtree` limitation for `Cookbook/Dinner recipes/Vegetarian`. Assign the role to the vegetarian editors user group. It allows users from that group to access the **Vegetarian** container but not **Cookbook** and **Dinner recipes**. To give users access to **Cookbook** and **Dinner recipes** containers, create a new role, for example, *EditorVegAccess*. Next, add to it a `content/read` policy with the `Location` limitations **Cookbook** and **Dinner recipes**. Assign the new role to the vegetarian editors user group as well. Only then the limitations are combined with `AND`, resulting in an empty set. The vegetarian editors should now see the following content tree: ![Limited subtree file structure](https://doc.ibexa.co/en/saas/permissions/img/subtree_usability_notes_2.png) When a policy has more than one limitation, all of them have to apply, or the policy doesn't work. For example, a `Location` limitation on location `1/2` and `Subtree of Location` limitation on `1/2/55` cannot work together, because no location can satisfy both those requirements at the same time. To combine more than one limitation with the *or* relation, not *and*, you can split your policy in two, each with one of these limitations. ## Manage locations To add a new location to a content item, the policies required for publishing content are enough. To allow the user to remove a location, grant them the following policies: - `content/remove` - `content/manage_locations` Hiding and revealing location requires one more policy: `content/hide`. ## Editorial workflows You can control which stages in an editorial workflow the user can work with. Do this by adding the `WorkflowStageLimitation` to `content` policies such as `content/edit` or `content/publish`. You can also control which transitions the user can pass content through. Do this by using the `workflow/change_stage` policy together with the `WorkflowTransitionLimitation`. For example, to enable the user to edit only content in the "Design" stage and to pass it after creating design to the "Proofread stage", use following permissions: - `content/edit` with `WorkflowStageLimitation` set to "Design". - `workflow/change_stage` with `WorkflowTransitionLimitation` set to `to_proofreading` ## Multi-file upload Creating content through multi-file upload is treated in the same way as regular creation. To enable upload, you need you set the following permissions: - `content/create` - `content/read` - `content/publish` You can control what content items can be uploaded and where by using imitations on the `content/create` and `content/publish` policies. A location limitation limits the uploading to a specific location in the tree. A content type limitation controls the content types that are allowed. For example, you can set the location limitation on a **Pictures** Folder, and add a content type limitation that only allows content items of type **Image**. This ensures that only files of type `image` can be uploaded, and only to the **Pictures** Folder. ## Taxonomies You can control which users or user groups can work with taxonomies. To let users create and assign taxonomy entries, set the following permissions: - `taxonomy/assign` to allow user to tag and untag content - `taxonomy/read` to see the Taxonomy interface - `taxonomy/manage` to create, edit and delete tags With limitations, you can configure whether permissions apply to Tags, product categories, or both. ## Register users To allow anonymous users to register through the `/register` route, grant the `user/register` policy to the Anonymous user group. ## Admin To access the [administration panel](https://doc.ibexa.co/en/saas/administration/admin_panel/admin_panel/index.md) in the back office, the User must have the `setup/administrate` policy. This allows the User to view the languages and content types. Additional policies are needed for each section of the Admin. ### System Information - `setup/system_info` to view the System Information tab ### Sections - `section/view` to see and access the section list - `section/edit` to add and edit sections - `section/assign` to assign sections to content ### Languages - `content/translations` to add and edit languages ### Content types/action - `content type/create`, `content type/update`, `content type/delete` to add, modify and remove content types ### Object states - `state/administrate` to view a list of object states, add and edit them - `state/assign` to assign Objects states to content ### Roles - `role/read` to view the list of roles in Admin - `role/create`, `role/update`, `role/assign` and `role/delete` to manage roles ### Users - `content/view` to view the list of users Users are treated like other content, so to create and modify them, the user needs to have the same permissions as for managing other content items. ## Product catalog You can control to what extend users can access the product catalog and all its related parts. ### Product type To create or edit product types, a user needs to have access to attributes and attribute groups. Set the following permissions to allow such access: - `product_type/create` - `product_type/view` - `product_type/edit` ### Product item When a product is created, a product item and a content item are also generated. Permissions for the product catalog override permissions for content, therefore, users without permissions for content can still manage products. - `product/create` - `product/view` - `product/edit` # Policies > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). Policies are the main building block of the permissions system which lets you define the accesses for specific user roles. Policies are the main building block of the permissions system. Each role you assign to user or user group consists of policies which define, which parts of the application or website the user has access to. ## Available policies ### Access to all functions | Module | Function | Effect | Possible limitations | | ------ | -------- | ----------------------------------------------------------- | -------------------- | | `*` | `*` | all modules, all functions: grant all available permissions | | > **Tip: Tip** > > For each module, all functions can be given without limitation. For example, `content/*` gives access to all functions of the `content` module, even future ones. ### Administration and user management #### Activity log | Module | Function | Effect | Possible Limitations | | -------------- | -------- | -------------------- | ---------------------------------------------------------------------------------------------------------------- | | `activity_log` | `read` | access activity list | [ActivityLogOwner](https://doc.ibexa.co/en/saas/permissions/limitation_reference/#activity-log-owner-limitation) | #### AI actions | Module | Function | Effect | Possible Limitations | | ---------------------- | --------- | ---------------------- | -------------------- | | `action_configuration` | `view` | view AI Action | | | | `create` | create a new AI action | | | | `edit` | edit an AI action | | | | `delete` | delete an AI action | | | | `execute` | execute an AI action | | #### Customer groups | Module | Function | Effect | Possible limitations | | ---------------- | -------- | ----------------------- | -------------------- | | `customer_group` | `create` | create a customer group | | | | `delete` | delete a customer group | | | | `edit` | edit a customer group | | | | `view` | view customer groups | | #### Roles | Module | Function | Effect | Possible limitations | | ------ | -------- | -------------------------------------------------------------------------- | -------------------- | | `role` | `assign` | assign roles to users and user groups | | | | `create` | create new roles | | | | `delete` | delete roles | | | | `read` | view the roles list in Admin. Required for all other role-related policies | | | | `update` | modify existing roles | | #### Segments | Module | Function | Effect | Possible limitations | | --------- | ---------------- | ------------------------ | -------------------------------------------------------------------------------------------------------- | | `segment` | `assign_to_user` | assign segments to users | [Segment Group](https://doc.ibexa.co/en/saas/permissions/limitation_reference/#segment-group-limitation) | | | `create` | create segments | [Segment Group](https://doc.ibexa.co/en/saas/permissions/limitation_reference/#segment-group-limitation) | | | `read` | load segment information | [Segment Group](https://doc.ibexa.co/en/saas/permissions/limitation_reference/#segment-group-limitation) | | | `remove` | remove segments | [Segment Group](https://doc.ibexa.co/en/saas/permissions/limitation_reference/#segment-group-limitation) | | | `update` | update segments | [Segment Group](https://doc.ibexa.co/en/saas/permissions/limitation_reference/#segment-group-limitation) | #### Segment groups | Module | Function | Effect | Possible limitations | | --------------- | -------- | ------------------------------ | -------------------- | | `segment_group` | `create` | create segment groups | | | | `read` | load segment group information | | | | `remove` | remove segment groups | | | | `update` | update segment groups | | #### Setup | Module | Function | Effect | Possible limitations | | ------- | -------------- | -------------------------------------------- | -------------------- | | `setup` | `administrate` | access Admin | | | | `install` | unused | | | | `setup` | unused | | | | `system_info` | view the **System Information** tab in Admin | | #### Sites (Experience) | Module | Function | Effect | Possible limitations | | ------ | --------------- | ---------------------------------------------------------------------------------------- | -------------------- | | `site` | `change_status` | change status of the public accesses of sites to `Live` or `Offline` in the Site Factory | | | | `create` | create sites in the Site Factory | | | | `delete` | delete sites from the Site Factory | | | | `edit` | edit sites in the Site Factory | | | | `update` | update sites in the Site Factory | | | | `view` | view the "Sites" in the top navigation | | #### Users | Module | Function | Effect | Possible limitations | | ------ | ------------- | ------------------------------------------------ | -------------------- | | `user` | `activation` | unused | | | | `invite` | create and send invitations to create an account | | | | `login` | log in to the application | | | | `password` | unused | | | | `preferences` | access and set user preferences | | | | `register` | register using the `/register` route | | | | `selfedit` | unused | | ### Content management #### Content | Module | Function | Effect | Possible limitations | | --------- | -------------------- | --------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `content` | `cleantrash` | empty the Trash (even when the User doesn't have access to individual content items) | | | | `create` | create new content. Note: even without this policy the user is able to enter edit mode, but cannot finalize work with the content item. | [Content type](https://doc.ibexa.co/en/saas/permissions/limitation_reference/#content-type-limitation) [Section](https://doc.ibexa.co/en/saas/permissions/limitation_reference/#section-limitation) [Location](https://doc.ibexa.co/en/saas/permissions/limitation_reference/#location-limitation) [Subtree](https://doc.ibexa.co/en/saas/permissions/limitation_reference/#subtree-limitation) [Language](https://doc.ibexa.co/en/saas/permissions/limitation_reference/#language-limitation) [Owner of Parent](https://doc.ibexa.co/en/saas/permissions/limitation_reference/#owner-of-parent-limitation) [Content type Group of Parent](https://doc.ibexa.co/en/saas/permissions/limitation_reference/#content-type-group-of-parent-limitation) [Content type of Parent](https://doc.ibexa.co/en/saas/permissions/limitation_reference/#content-type-of-parent-limitation) [Parent Depth](https://doc.ibexa.co/en/saas/permissions/limitation_reference/#parent-depth-limitation) [Field Group](https://doc.ibexa.co/en/saas/permissions/limitation_reference/#field-group-limitation) [Change Owner](https://doc.ibexa.co/en/saas/permissions/limitation_reference/#change-owner-limitation) | | | `diff` | unused | | | | `edit` | edit existing content | [Content type](https://doc.ibexa.co/en/saas/permissions/limitation_reference/#content-type-limitation) [Section](https://doc.ibexa.co/en/saas/permissions/limitation_reference/#section-limitation) [Owner](https://doc.ibexa.co/en/saas/permissions/limitation_reference/#owner-limitation) [Content type Group](https://doc.ibexa.co/en/saas/permissions/limitation_reference/#content-type-group-limitation) [Location](https://doc.ibexa.co/en/saas/permissions/limitation_reference/#location-limitation) [Subtree](https://doc.ibexa.co/en/saas/permissions/limitation_reference/#subtree-limitation) [Language](https://doc.ibexa.co/en/saas/permissions/limitation_reference/#language-limitation) [Object State](https://doc.ibexa.co/en/saas/permissions/limitation_reference/#object-state-limitation) [Workflow Stage](https://doc.ibexa.co/en/saas/permissions/limitation_reference/#workflow-stage-limitation) [Field Group](https://doc.ibexa.co/en/saas/permissions/limitation_reference/#field-group-limitation) [Version Lock](https://doc.ibexa.co/en/saas/permissions/limitation_reference/#version-lock-limitation) [Change Owner](https://doc.ibexa.co/en/saas/permissions/limitation_reference/#change-owner-limitation) | | | `hide` | hide and reveal content locations | [Content type](https://doc.ibexa.co/en/saas/permissions/limitation_reference/#content-type-limitation) [Section](https://doc.ibexa.co/en/saas/permissions/limitation_reference/#section-limitation) [Owner](https://doc.ibexa.co/en/saas/permissions/limitation_reference/#owner-limitation) [Content type Group](https://doc.ibexa.co/en/saas/permissions/limitation_reference/#content-type-group-limitation) [Location](https://doc.ibexa.co/en/saas/permissions/limitation_reference/#location-limitation) [Subtree](https://doc.ibexa.co/en/saas/permissions/limitation_reference/#subtree-limitation) [Language](https://doc.ibexa.co/en/saas/permissions/limitation_reference/#language-limitation) | | | `manage_locations` | remove locations and send content to Trash | [Content type](https://doc.ibexa.co/en/saas/permissions/limitation_reference/#content-type-limitation) [Section](https://doc.ibexa.co/en/saas/permissions/limitation_reference/#section-limitation) [Owner](https://doc.ibexa.co/en/saas/permissions/limitation_reference/#owner-limitation) [Subtree](https://doc.ibexa.co/en/saas/permissions/limitation_reference/#subtree-limitation) [Object State](https://doc.ibexa.co/en/saas/permissions/limitation_reference/#object-state-limitation) | | | `pendinglist` | unused | | | | `publish` | publish content. Without this Policy, the User can only save drafts or send them for review (in Ibexa Experience) | [Content type](https://doc.ibexa.co/en/saas/permissions/limitation_reference/#content-type-limitation) [Section](https://doc.ibexa.co/en/saas/permissions/limitation_reference/#section-limitation) [Owner](https://doc.ibexa.co/en/saas/permissions/limitation_reference/#owner-limitation) [Content type Group](https://doc.ibexa.co/en/saas/permissions/limitation_reference/#content-type-group-limitation) [Location](https://doc.ibexa.co/en/saas/permissions/limitation_reference/#location-limitation) [Subtree](https://doc.ibexa.co/en/saas/permissions/limitation_reference/#subtree-limitation) [Language](https://doc.ibexa.co/en/saas/permissions/limitation_reference/#language-limitation) [Object State](https://doc.ibexa.co/en/saas/permissions/limitation_reference/#object-state-limitation) [Workflow Stage](https://doc.ibexa.co/en/saas/permissions/limitation_reference/#workflow-stage-limitation) | | | `read` | view the content both in front and back end | [Content type](https://doc.ibexa.co/en/saas/permissions/limitation_reference/#content-type-limitation) [Section](https://doc.ibexa.co/en/saas/permissions/limitation_reference/#section-limitation) [Owner](https://doc.ibexa.co/en/saas/permissions/limitation_reference/#owner-limitation) [Content type Group](https://doc.ibexa.co/en/saas/permissions/limitation_reference/#content-type-group-limitation) [Location](https://doc.ibexa.co/en/saas/permissions/limitation_reference/#location-limitation) [Subtree](https://doc.ibexa.co/en/saas/permissions/limitation_reference/#subtree-limitation) [Object State](https://doc.ibexa.co/en/saas/permissions/limitation_reference/#object-state-limitation) | | | `remove` | remove locations and send content to Trash | [Content type](https://doc.ibexa.co/en/saas/permissions/limitation_reference/#content-type-limitation) [Section](https://doc.ibexa.co/en/saas/permissions/limitation_reference/#section-limitation) [Owner](https://doc.ibexa.co/en/saas/permissions/limitation_reference/#owner-limitation) [Location](https://doc.ibexa.co/en/saas/permissions/limitation_reference/#location-limitation) [Subtree](https://doc.ibexa.co/en/saas/permissions/limitation_reference/#subtree-limitation) [Object State](https://doc.ibexa.co/en/saas/permissions/limitation_reference/#object-state-limitation) [Language](https://doc.ibexa.co/en/saas/permissions/limitation_reference/#language-limitation) | | | `restore` | restore content from Trash | | | | `reverserelatedlist` | see all content that a content item relates to (even when the User isn't allowed to view it as an individual content items) | [Content type](https://doc.ibexa.co/en/saas/permissions/limitation_reference/#content-type-limitation) [Section](https://doc.ibexa.co/en/saas/permissions/limitation_reference/#section-limitation) | | | `translate` | unused | [Content type](https://doc.ibexa.co/en/saas/permissions/limitation_reference/#content-type-limitation) [Section](https://doc.ibexa.co/en/saas/permissions/limitation_reference/#section-limitation) [Owner](https://doc.ibexa.co/en/saas/permissions/limitation_reference/#owner-limitation) [Location](https://doc.ibexa.co/en/saas/permissions/limitation_reference/#location-limitation) [Subtree](https://doc.ibexa.co/en/saas/permissions/limitation_reference/#subtree-limitation) [Language](https://doc.ibexa.co/en/saas/permissions/limitation_reference/#language-limitation) | | | `translations` | manage the language list in Admin | | | | `unlock` | unlock drafts locked to a user for performing actions | [Owner](https://doc.ibexa.co/en/saas/permissions/limitation_reference/#owner-limitation) [Content type Group](https://doc.ibexa.co/en/saas/permissions/limitation_reference/#content-type-group-limitation) [Subtree](https://doc.ibexa.co/en/saas/permissions/limitation_reference/#subtree-limitation) [Language](https://doc.ibexa.co/en/saas/permissions/limitation_reference/#language-limitation) [Version Lock](https://doc.ibexa.co/en/saas/permissions/limitation_reference/#version-lock-limitation) | | | `urltranslator` | manage URL aliases of a content item | | | | `versionread` | view content after publishing, and to preview any content in the Site mode | [Content type](https://doc.ibexa.co/en/saas/permissions/limitation_reference/#content-type-limitation) [Section](https://doc.ibexa.co/en/saas/permissions/limitation_reference/#section-limitation) [Owner](https://doc.ibexa.co/en/saas/permissions/limitation_reference/#owner-limitation) Status [Location](https://doc.ibexa.co/en/saas/permissions/limitation_reference/#location-limitation) [Subtree](https://doc.ibexa.co/en/saas/permissions/limitation_reference/#subtree-limitation) [Object State](https://doc.ibexa.co/en/saas/permissions/limitation_reference/#object-state-limitation) | | | `versionremove` | remove archived content versions | [Content type](https://doc.ibexa.co/en/saas/permissions/limitation_reference/#content-type-limitation) [Section](https://doc.ibexa.co/en/saas/permissions/limitation_reference/#section-limitation) [Owner](https://doc.ibexa.co/en/saas/permissions/limitation_reference/#owner-limitation) Status [Location](https://doc.ibexa.co/en/saas/permissions/limitation_reference/#location-limitation) [Subtree](https://doc.ibexa.co/en/saas/permissions/limitation_reference/#subtree-limitation) [Object State](https://doc.ibexa.co/en/saas/permissions/limitation_reference/#object-state-limitation) | | | `view_embed` | view content embedded in another content item (even when the User isn't allowed to view it as an individual content item) | [Content type](https://doc.ibexa.co/en/saas/permissions/limitation_reference/#content-type-limitation) [Section](https://doc.ibexa.co/en/saas/permissions/limitation_reference/#section-limitation) [Owner](https://doc.ibexa.co/en/saas/permissions/limitation_reference/#owner-limitation) [Location](https://doc.ibexa.co/en/saas/permissions/limitation_reference/#location-limitation) [Subtree](https://doc.ibexa.co/en/saas/permissions/limitation_reference/#subtree-limitation) | #### Content types | Module | Function | Effect | Possible limitations | | ------- | -------- | ------------------------------------------------------------------------ | -------------------- | | `class` | `create` | create new content types. Also required to edit exiting content types | | | | `delete` | delete content types | | | | `update` | modify existing content types. Also required to create new content types | | #### Sections | Module | Function | Effect | Possible limitations | | --------- | -------- | -------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `section` | `assign` | assign Sections to content | [content type](https://doc.ibexa.co/en/saas/permissions/limitation_reference/#content-type-limitation) [Section](https://doc.ibexa.co/en/saas/permissions/limitation_reference/#section-limitation) [Owner](https://doc.ibexa.co/en/saas/permissions/limitation_reference/#owner-limitation) [New Section](https://doc.ibexa.co/en/saas/permissions/limitation_reference/#new-section-limitation) | | | `edit` | edit existing Sections and create new ones | | | | `view` | view the Sections list in Admin. Required for all other section-related policies | | #### Object States | Module | Function | Effect | Possible limitations | | ------- | -------------- | ------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `state` | `assign` | assign object states to content items | [Content type](https://doc.ibexa.co/en/saas/permissions/limitation_reference/#content-type-limitation) [Section](https://doc.ibexa.co/en/saas/permissions/limitation_reference/#section-limitation) [Owner](https://doc.ibexa.co/en/saas/permissions/limitation_reference/#owner-limitation) [Content type Group](https://doc.ibexa.co/en/saas/permissions/limitation_reference/#content-type-group-limitation) [Location](https://doc.ibexa.co/en/saas/permissions/limitation_reference/#location-limitation) [Subtree](https://doc.ibexa.co/en/saas/permissions/limitation_reference/#subtree-limitation) [Object State](https://doc.ibexa.co/en/saas/permissions/limitation_reference/#object-state-limitation) [New State](https://doc.ibexa.co/en/saas/permissions/limitation_reference/#new-state-limitation) | | | `administrate` | view, add and edit object states | | #### Taxonomy | Module | Function | Effect | Possible limitations | | ---------- | -------- | ----------------------------- | -------------------- | | `taxonomy` | `assign` | tag or untag content | | | | `manage` | create, edit, and delete tags | | | | `read` | view the Taxonomy interface | | #### Workflow and version comparison | Module | Function | Effect | Possible limitations | | ------------ | -------------- | -------------------------------------- | -------------------------------------------------------------------------------------------------------------------- | | `comparison` | `view` | view version comparison | | | `workflow` | `change_stage` | change stage in the specified workflow | [Workflow Transition](https://doc.ibexa.co/en/saas/permissions/limitation_reference/#workflow-transition-limitation) | ### Product catalog #### Catalogs | Module | Function | Effect | Possible limitations | | --------- | -------- | ---------------- | -------------------- | | `catalog` | `create` | create a catalog | | | | `delete` | delete a catalog | | | | `edit` | edit a catalog | | | | `view` | view catalogs | | #### Currencies and regions | Module | Function | Effect | Possible limitations | | ---------- | ---------- | ----------------- | -------------------- | | `commerce` | `currency` | manage currencies | | | | `region` | manage regions | | #### Products | Module | Function | Effect | Possible limitations | | --------- | -------- | ------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `product` | `create` | create a product | [Product Type](https://doc.ibexa.co/en/saas/permissions/limitation_reference/#product-type-limitation) [Language](https://doc.ibexa.co/en/saas/permissions/limitation_reference/#language-limitation) | | | `delete` | delete a product | [Product Type](https://doc.ibexa.co/en/saas/permissions/limitation_reference/#product-type-limitation) | | | `edit` | edit a product | [Product Type](https://doc.ibexa.co/en/saas/permissions/limitation_reference/#product-type-limitation) [Language](https://doc.ibexa.co/en/saas/permissions/limitation_reference/#language-limitation) | | | `view` | view products listed in the product catalog | [Product Type](https://doc.ibexa.co/en/saas/permissions/limitation_reference/#product-type-limitation) | > **Caution: Caution** > > The `ProductType` limitation can't be used when using [Quable](https://doc.ibexa.co/en/saas/product_catalog/quable/quable/index.md). #### Product types | Module | Function | Effect | Possible limitations | | -------------- | -------- | ---------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------ | | `product_type` | `create` | create a product type, a new attribute, a new attribute group, and add translation to product type and attribute | [Product Type](https://doc.ibexa.co/en/saas/permissions/limitation_reference/#product-type-limitation) | | | `delete` | delete a product type, attribute, attribute group | | | | `edit` | edit a product type, attribute, attribute group | [Product Type](https://doc.ibexa.co/en/saas/permissions/limitation_reference/#product-type-limitation) | | | `view` | view product types, attributes and attribute groups | | > **Caution: Caution** > > The `ProductType` limitation can't be used when using [Quable](https://doc.ibexa.co/en/saas/product_catalog/quable/quable/index.md). ## Combining policies Policies on one role are connected with the *and* relation, not *or*, so when policy has more than one limitation, all of them have to apply. If you want to combine more than one limitation with the *or* relation, not *and*, you can split your policy in two, each with one of these limitations. # Limitations > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). Control access to parts of the system by fine-tuning permissions with the use of Limitations. Limitations are part of the permissions system. They limit the access granted to users by [policies](https://doc.ibexa.co/en/saas/permissions/permission_overview/index.md). While a policy grants the user access to a function, Limitations narrow it down by different criteria. Limitations consist of two parts: - `Limitation` (Value) - `LimitationType` Certain limitations also serve as role limitations, which means they can be used to limit the rights of a role assignment. Currently, this covers [subtree of location](https://doc.ibexa.co/en/saas/permissions/limitation_reference/#subtree-limitation) and [Section](https://doc.ibexa.co/en/saas/permissions/limitation_reference/#section-limitation). `Limitation` represents the value, while `LimitationType` deals with the business logic surrounding how it actually works and is enforced. `LimitationTypes` have two modes of operation in regard to permission logic (see [`Ibexa\Contracts\Core\Limitation`](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-Core-Limitation-Type.html) interface for more info): | Method | Use | | -------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `evaluate` | Evaluates if the User has access to a given object in a certain context (for instance the context can be locations when the object is `Content`), under the condition of the `Limitation` value(s). | | `getCriterion` | Generates a `Criterion` based on `Limitation` value and current user which `SearchService` by default applies to Search Criteria for filtering search based on permissions. | ## Limitation reference See [Limitation reference](https://doc.ibexa.co/en/saas/permissions/limitation_reference/index.md) for detailed information about individual limitations. # Limitation reference > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). Limitations let you fine-tune the permission system by specifying limits to roles granted to users. ## Blocking limitation A generic limitation type to use when no other limitation has been implemented. Without any limitation assigned, a `LimitationNotFoundException` is thrown. It's called "blocking" because it always informs the permissions system that the user doesn't have access to any policy the limitation is assigned to, making the permissions system move on to the next policy. ### Possible values | Value | UI value | Description | | --------- | --------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `` | `` | This is a generic limitation which doesn't validate the values provided to it. Make sure that you validate the values passed to this limitation in your own logic. | ### Configuration As this is a generic limitation, you can configure your custom limitations to use it. Out of the box FunctionList uses it in the following way: ```yaml # FunctionList is an ezjscore limitation, it only applies to ezjscore policies not used by # API/platform stack, so configure to use Blocking limitation to avoid LimitationNotFoundException ibexa.api.role.limitation_type.function_list: class: Ibexa\Core\Limitation\BlockingLimitationType arguments: ['FunctionList'] tags: - {name: ibexa.permissions.limitation_type, alias: FunctionList} ``` ## Activity log Owner limitation The Activity log Owner (`ActivityLogOwner`) limitation specifies if a user can see only their own [recent activity](https://doc.ibexa.co/en/saas/administration/recent_activity/recent_activity/index.md) log entries, and not entries from other users. | Value | UI value | Description | | ----- | --------------- | ------------------------------------------------------------ | | `1` | "Only own logs" | Current user can only access their own activity log entries. | ## Change Owner limitation The Change Owner (`ChangeOwner`) limitation specifies whether the user can change the owner of a content item. ### Possible values | Value | UI value | Description | | ----- | -------- | ---------------------------------------------- | | `1` | "Forbid" | The user cannot change owner of a content item | ## Content type Group limitation The Content Type Group (`UserGroup`) limitation specifies that only users with at least one common *direct* user group with the owner of content get the selected access right. ### Possible values | Value | UI value | Description | | ----- | -------- | -------------------------------------------------------------------------------------- | | `1` | "self" | Only a user who has at least one common *direct* user group with the owner gets access | ## Content type Group of Parent limitation The Content Type Group of Parent (`ParentUserGroupLimitation`) limitation specifies that only Users with at least one common *direct* user group with the owner of the parent location of a content item get a certain access right, used by `content/create` permission. ### Possible values | Value | UI value | Description | | ----- | -------- | --------------------------------------------------------------------------------------------------------- | | `1` | "self" | Only a user who has at least one common *direct* user group with owner of the parent location gets access | ## Content type limitation The Content Type (`ContentType`) limitation specifies whether the user has access to content with a specific content type. ### Possible values | Value | UI value | Description | | ------------------ | -------------------- | ------------------------------------------------- | | `` | `` | All valid content type IDs can be set as value(s) | ## Content type of Parent limitation The Content Type of Parent (`ParentContentType`) limitation specifies whether the user has access to content whose parent location contains a specific content type, used by `content/create`. This limitation combined with `ContentType` limitation allows you to define business rules like allowing users to create "Blog Post" within a "Blog." If you also combine it with `Owner of Parent` limitation, you effectively limit access to create Blog Posts in the users' own Blogs. ### Possible values | Value | UI value | Description | | ------------------ | -------------------- | ------------------------------------------------- | | `` | `` | All valid content type IDs can be set as value(s) | ## Field Group limitation (Experience) A Field Group (`FieldGroup`) limitation specifies whether the user can work with content fields belonging to a specific group. A user with this limitation is allowed to edit fields belonging to the indicated group. Otherwise, the fields are inactive and filled with the default value (if set). ### Possible values | Value | UI value | Description | | ------------------------- | ------------------------- | -------------------------------------------------------- | | `` | `` | All valid field group identifiers can be set as value(s) | ## Language limitation A Language (`Language`) limitation specifies whether the user has access to work on the specified translation. A user with this limitation is allowed to: - Create new content with the given translation(s) only. This only applies to creating the first version of a content item. - Edit content by adding a new translation or modifying an existing translation. - Publish content only when it results in adding or modifying an allowed translation. - Delete content only when it contains a translation into the specified language. ### Possible values | Value | UI value | Description | | ----------------- | --------------------- | ----------------------------------------------- | | `` | `` | All valid language codes can be set as value(s) | ## Location limitation A location (`Location`) limitation specifies whether the user has access to content with a specific location, in case of `content/create` the parent location is evaluated. ### Possible values | Value | UI value | Description | | --------------- | ----------------- | --------------------------------------------- | | `` | `` | All valid location IDs can be set as value(s) | ## New Section limitation A New Section (`NewSection`) limitation specifies whether the user has access to assigning content to a given section. In the `section/assign` policy you can combine this with section limitation to limit both from and to values. ### Possible values | Value | UI value | Description | | -------------- | ---------------- | -------------------------------------------- | | `` | `` | All valid session IDs can be set as value(s) | ## New State limitation A New State (`NewObjectState`) limitation specifies whether the user has access to (assigning) a given object state to content. In the `state/assign` policy you can combine this with State limitation to limit both from and to values. ### Possible values | Value | UI value | Description | | ------------ | -------------- | ------------------------------------------ | | `` | `` | All valid state IDs can be set as value(s) | ## Object State limitation The Object State (`ObjectState`) limitation specifies whether the user has access to content with a specific object state. ### Possible values | Value | UI value | Description | | ------------------ | -------------------- | ------------------------------------------------- | | `` | `` | All valid Object state IDs can be set as value(s) | ## Owner limitation The Owner (`Owner`) limitation specifies that only the owner of the content item gets the selected access right. ### Possible values | Value | UI value | Description | | ----- | --------- | ----------------------------------------------------------------------------------------------------- | | `1` | "self" | Only the user who is the owner gets access | | `2` | "session" | Deprecated and works exactly like "self" in public PHP API since it has no knowledge of user Sessions | ## Owner of Parent limitation The Owner of Parent (`ParentOwner`) limitation specifies that only the users who own all parent locations of a content item get a certain access right, used for `content/create` permission. ### Possible values | Value | UI value | Description | | ----- | --------- | ----------------------------------------------------------------------------------------------------- | | `1` | "self" | Only the user who is the owner of all parent locations gets access | | `2` | "session" | Deprecated and works exactly like "self" in public PHP API since it has no knowledge of user Sessions | ## Parent Depth limitation The Parent Depth (`ParentDepth`) limitation specifies whether the user has access to creating content under a parent location within a specific depth of the tree, used for `content/create` permission. ### Possible values | Value | UI value | Description | | ------- | -------- | ----------------------------------------- | | `` | `` | All valid integers can be set as value(s) | ## Product Type limitation The Product Type (`ProductType`) limitation specifies whether the user has access to products belonging to a specific product type. > **Caution: Caution** > > The `ProductType` limitation can't be used when using [Quable](https://doc.ibexa.co/en/saas/product_catalog/quable/quable/index.md). ### Possible values | Value | UI value | Description | | ------------------ | -------------------- | ------------------------------------------------- | | `` | `` | All valid content type IDs can be set as value(s) | ## Section limitation The Section (`Section`) limitation specifies whether the user has access to content within a specific section. This limitation can be used as a role limitation. ### Possible values | Value | UI value | Description | | -------------- | ---------------- | -------------------------------------------- | | `` | `` | All valid session IDs can be set as value(s) | ## Segment group limitation (Experience) The segment group (`SegmentGroup`) limitation specifies whether the user has access segments within a specific segment group. This limitation can be used as a role limitation. ### Possible values | Value | UI value | Description | | -------------------- | ---------------------- | --------------------------------------------------- | | `` | `` | All valid segment group IDs can be set as value(s). | ## SiteAccess limitation The SiteAccess (`SiteAccess`) limitation specifies to which SiteAccesses a certain permission applies, used by `user/login`. ### Possible values | Value | UI value | Description | | ------------------- | ------------------- | -------------------------------------------------------------------------------------------------------------------- | | `` | `` | Hash is calculated in the following way in legacy in default 64bit mode: `sprintf( '%u', crc32( $siteAccessName ) )` | ### Legacy compatibility notes `SiteAccess` limitation is deprecated and isn't used actively in public PHP API, but is allowed for being able to read / create limitations for legacy. ## Subtree limitation The subtree (`Subtree`) limitation specifies whether the user has access to content within a specific subtree of location, in case of `content/create` the parent subtree of location is evaluated. This limitation can be used as a role limitation. ### Possible values | Value | UI value | Description | | ----------------------- | ----------------- | ------------------------------------------------------- | | `` | `` | All valid location `pathStrings` can be set as value(s) | ### Usage notes For more information on how to restrict user's access to part of the subtree, see [the example in the Admin management section](https://doc.ibexa.co/en/saas/permissions/permission_use_cases/#restrict-editing-to-part-of-the-tree). ## Taxonomy limitation The taxonomy (`Taxonomy`) limitation specifies with which [taxonomies](https://doc.ibexa.co/en/saas/content_management/taxonomy/taxonomy/index.md) (tags, product categories, or custom ones) user can interact. The supported policies are: - `taxonomy/read` - `taxonomy/manage` - `taxonomy/assign` ### Possible values | Value | UI value | Description | | -------------------- | -------------- | -------------------------- | | Taxonomy identifiers | Taxonomy names | List of allowed taxonomies | ## Taxonomy Subtree limitation The taxonomy subtree (`TaxonomySubtree`) limitation specifies whether the user has access to a specific subtree within the [taxonomy](https://doc.ibexa.co/en/saas/content_management/taxonomy/taxonomy/index.md) tree. Once a tag is selected, user can interact with it and all the child tags below it in the taxonomy tree. In addition, it grants read-only access to all the parent tags (up to the taxonomy root) so that the user can see the context. The supported policies are: - `taxonomy/read` - `taxonomy/manage` - `taxonomy/assign` ### Possible values | Value | UI value | Description | | ------- | ------------- | ----------------------------- | | Tag IDs | Selected tags | All valid Tag IDs are allowed | ## Version Lock limitation The Version Lock (`VersionLock`) limitation specifies whether the user can perform actions, for example, edit or unlock, on content items that are in a workflow. This limitation can be used as a role limitation. ### Possible values | Value | UI value | Description | | -------- | --------------- | ----------------------------------------------------------------------------------------------------------- | | `userId` | "Assigned only" | Users can perform actions only on content items that are assigned to them or not assigned to anybody. | | `null` | none | Users can perform actions on all drafts, regardless of the assignments or whether drafts are locked or not. | ## Workflow Stage limitation The Workflow Stage (`WorkflowStage`) limitation specifies whether the user can edit content in a specific workflow stage. ### Possible values The limitation takes as values stages configured for the workflow. ## Workflow Transition limitation The Workflow Transition (`WorkflowTransition`) limitation specifies whether the user can move the content in a workflow through a specific transition. ### Possible values The limitation takes as values transitions between stages configured for the workflow. # Custom policies > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). Create a custom policy to cover non-standard permission needs. The content repository uses [roles and policies](https://doc.ibexa.co/en/saas/permissions/permissions/index.md) to give users access to different functions of the system. Any bundle can expose available policies via a `PolicyProvider` which can be added to IbexaCoreBundle's [service container](https://doc.ibexa.co/en/saas/api/php_api/php_api/#service-container) extension. ## PolicyProvider A `PolicyProvider` object provides a hash containing declared modules, functions and limitations. - Each policy provider provides a collection of permission *modules*. - Each module can provide *functions* (for example, in `content/read`, "content" is the module, and "read" is the function) - Each function can provide a collection of limitations. First level key is the module name which is limited to characters within the set `A-Za-z0-9_`, value is a hash of available functions, with function name as key. Function value is an array of available limitations, identified by the alias declared in `LimitationType` service tag. If no limitation is provided, value can be `null` or an empty array. ```php $config = [ 'content' => [ 'read' => ['Class', 'ParentClass', 'Node', 'Language'], 'edit' => ['Class', 'ParentClass', 'Language'], ], 'custom_module' => [ 'custom_function_1' => null, 'custom_function_2' => ['CustomLimitation'], ], ]; ``` Limitations need to be implemented as *Limitation types* and declared as services identified with `ibexa.permissions.limitation_type` tag. Name provided in the hash for each limitation is the same value set in the `alias` attribute in the service tag. For example: ```php addConfig([ 'custom_module' => [ 'custom_function_1' => null, 'custom_function_2' => ['CustomLimitation'], ], ]); } } ``` > **Note: Extend existing policies** > > While a `PolicyProvider` may provide new functions to an existing policy module, or additional limitations to an existing function, it's however strongly recommended to create your own modules. > > It's impossible to remove an existing module, function or limitation from a policy. ### YamlPolicyProvider An abstract class based on YAML is provided: `Ibexa\Bundle\Core\DependencyInjection\Security\PolicyProvider\YamlPolicyProvider`. It defines an abstract `getFiles()` method. Extend `YamlPolicyProvider` and implement `getFiles()` to return absolute paths to your YAML files. ```php addConfig([ 'custom_module' => [ 'custom_function_1' => null, 'custom_function_2' => ['CustomLimitation'], ], ]); } /** @return array<\JMS\TranslationBundle\Model\Message> */ public static function getTranslationMessages(): array { return [ (new Message('role.policy.custom_module', 'forms'))->setDesc('Custom module'), (new Message('role.policy.custom_module.all_functions', 'forms'))->setDesc('Custom module / All functions'), (new Message('role.policy.custom_module.custom_function_1', 'forms'))->setDesc('Custom module / Function #1'), (new Message('role.policy.custom_module.custom_function_2', 'forms'))->setDesc('Custom module / Function #2'), ]; } } ``` Then, extract this translation to generate the English translation file `translations/forms.en.xlf`: ```bash php bin/console jms:translation:extract en --domain=forms --dir=src --output-dir=translations ``` ## `PolicyProvider` integration into `IbexaCoreBundle` For a `PolicyProvider` to be active, you have to register it in the `src/Kernel.php`: ```php getExtension('ibexa'); // Add the policy provider, you can register multiple providers by calling the method repeatedly $ibexaExtension->addPolicyProvider(new MyPolicyProvider()); } } ``` ## Custom limitation type For a custom module function, you can use existing limitation types or create custom ones. The base of a custom limitation is a class to store values for the usage of this limitation in roles, and a class to implement the limitation's logic. The value class extends `Ibexa\Contracts\Core\Repository\Values\User\Limitation` and says for which limitation it's used: ```php limitationValues)) { $validationErrors[] = new ValidationError("limitationValues['value'] is missing."); } elseif (!is_bool($limitationValue->limitationValues['value'])) { $validationErrors[] = new ValidationError("limitationValues['value'] is not a boolean."); } return $validationErrors; } public function buildValue(array $limitationValues): CustomLimitationValue { $value = false; if (array_key_exists('value', $limitationValues)) { $value = $limitationValues['value']; } elseif (count($limitationValues)) { $value = (bool)$limitationValues[0]; } return new CustomLimitationValue(['limitationValues' => ['value' => $value]]); } /** * @param \Ibexa\Contracts\Core\Repository\Values\ValueObject[]|null $targets * * @return bool|null */ public function evaluate(Limitation $value, UserReference $currentUser, object $object, ?array $targets = null): ?bool { if (!$value instanceof CustomLimitationValue) { throw new InvalidArgumentException('$value', 'Must be of type: CustomLimitationValue'); } if ($value->limitationValues['value']) { return Type::ACCESS_GRANTED; } // If the limitation value is not set to `true`, then $currentUser, $object and/or $targets could be challenged to determine if the access is granted or not; Here or elsewhere. When passing the baton, a limitation can return Type::ACCESS_ABSTAIN return Type::ACCESS_DENIED; } public function getCriterion(Limitation $value, UserReference $currentUser): CriterionInterface { throw new NotImplementedException(__METHOD__); } public function valueSchema(): never { throw new NotImplementedException(__METHOD__); } } ``` The type class is set as a service tagged `ibexa.permissions.limitation_type` with an alias to identify it, and to link it to the value. ```yaml services: # … App\Security\Limitation\CustomLimitationType: tags: - { name: 'ibexa.permissions.limitation_type', alias: 'CustomLimitation' } ``` ### Custom limitation type form #### Form mapper To provide support for editing custom policies in the back office, you need to implement [`Ibexa\AdminUi\Limitation\LimitationFormMapperInterface`](https://github.com/ibexa/admin-ui/blob/6.0/src/lib/Limitation/LimitationFormMapperInterface.php). - `mapLimitationForm` adds the limitation field as a child to a provided Symfony form. - `getFormTemplate` returns the path to the template to use for rendering the limitation form. Here it use [`form_label`](https://symfony.com/doc/7.4/form/form_customization.html#reference-forms-twig-label) and [`form_widget`](https://symfony.com/doc/7.4/form/form_customization.html#reference-forms-twig-widget) to do so. - `filterLimitationValues` is triggered when the form is submitted and can manipulate the limitation values, such as normalizing them. ```php add('limitationValues', CheckboxType::class, [ 'label' => LimitationIdentifierToLabelConverter::convert($data->getIdentifier()), 'required' => false, 'data' => $data->limitationValues['value'], 'property_path' => 'limitationValues[value]', ]); } public function getFormTemplate(): string { return '@ibexadesign/limitation/custom_limitation_form.html.twig'; } public function filterLimitationValues(Limitation $limitation): void { } } ``` Provide a template corresponding to `getFormTemplate`. ```html+twig {# templates/themes/admin/limitation/custom_limitation_form.html.twig #} {{ form_label(form.limitationValues) }} {{ form_widget(form.limitationValues) }} ``` Next, register the service with the `ibexa.admin_ui.limitation.mapper.form` tag and set the `limitationType` attribute to the limitation type's identifier: ```yaml App\Security\Limitation\Mapper\CustomLimitationFormMapper: tags: - { name: 'ibexa.admin_ui.limitation.mapper.form', limitationType: 'CustomLimitation' } ``` #### Notable form mappers to extend Some abstract limitation type form mapper classes are provided to help implementing common complex limitations. - `MultipleSelectionBasedMapper` is a mapper used to build forms for limitations based on a checkbox list, where multiple items can be chosen. For example, it's used to build forms for [Content Type Limitation](https://doc.ibexa.co/en/saas/permissions/limitation_reference/#content-type-limitation), [Language Limitation](https://doc.ibexa.co/en/saas/permissions/limitation_reference/#language-limitation) or [Section Limitation](https://doc.ibexa.co/en/saas/permissions/limitation_reference/#section-limitation). - `UDWBasedMapper` is used to build a limitation form where a content/location must be selected. For example, it's used by the [Subtree Limitation](https://doc.ibexa.co/en/saas/permissions/limitation_reference/#subtree-limitation) form. #### Value mapper By default, without a value mapper, the limitation value is rendered by using the block `ibexa_limitation_value_fallback` of the template [`vendor/ibexa/admin-ui/src/bundle/Resources/views/themes/admin/limitation/limitation_values.html.twig`](https://github.com/ibexa/admin-ui/blob/v5.0.10/src/bundle/Resources/views/themes/admin/limitation/limitation_values.html.twig). To customize the rendering, a value mapper eventually transforms the limitation value and sends it to a custom template. The value mapper implements [`Ibexa\AdminUi\Limitation\LimitationValueMapperInterface`](https://github.com/ibexa/admin-ui/blob/4.5/src/lib/Limitation/LimitationValueMapperInterface.php). Its `mapLimitationValue` function returns the limitation value transformed for the needs of the template. ```php */ public function mapLimitationValue(Limitation $limitation): array { return [$limitation->limitationValues['value']]; } } ``` Then register the service with the `ibexa.admin_ui.limitation.mapper.value` tag and set the `limitationType` attribute to limitation type's identifier: ```yaml App\Security\Limitation\Mapper\CustomLimitationValueMapper: tags: - { name: 'ibexa.admin_ui.limitation.mapper.value', limitationType: 'CustomLimitation' } ``` When a value mapper exists for a limitation, the rendering uses a Twig block named `ibexa_limitation__value` where `` is the limitation identifier in lower case. In this example, block name is `ibexa_limitation_customlimitation_value` as the identifier is `CustomLimitation`. This template receives a `values` variable which is the return value of the `mapLimitationValue` function from the corresponding value mapper. ```html+twig {# templates/themes/standard/limitation/custom_limitation_value.html.twig #} {% block ibexa_limitation_customlimitation_value %} {% set is_set = values | first %} {{ is_set ? 'Yes' : 'No' }} {% endblock %} ``` To have your block found, you have to register its template. Add the template to the configuration under `ibexa.system..limitation_value_templates`: ```yaml ibexa: system: default: limitation_value_templates: - { template: '@ibexadesign/limitation/custom_limitation_value.html.twig', priority: 0 } ``` Provide translations for your custom limitation form in the `ibexa_content_forms_policies` domain. For example, `translations/ibexa_content_forms_policies.en.yaml`: ```yaml policy.limitation.identifier.customlimitation: 'Custom limitation' ``` ### Custom limitation check Check if current user has this custom limitation set to true from a custom controller: ```php getCustomLimitationValue()) { // Action only for user having the custom limitation checked } return new Response('...'); } private function getCustomLimitationValue(): bool { $hasAccess = $this->permissionResolver->hasAccess('custom_module', 'custom_function_2'); if (is_bool($hasAccess)) { return $hasAccess; } $customLimitationValues = $this->permissionChecker->getRestrictions( $hasAccess, CustomLimitationValue::class ); return $customLimitationValues['value'] ?? false; } #[\Override] public function performAccessCheck(): void { $this->traitPerformAccessCheck(); $this->denyAccessUnlessGranted(new Attribute('custom_module', 'custom_function_2')); } } ``` ## Restrict access to form submissions By default, access to a [Form content item](https://doc.ibexa.co/en/saas/content_management/forms/form_builder_guide/#forms-management) is controlled by the `content/read` policy. As a result, all users who can view a form in the back office can also [access](https://doc.ibexa.co/en/saas/content_management/forms/form_builder_guide/#view-results) its [**Submissions** tab](https://doc.ibexa.co/en/saas/administration/back_office/back_office_tabs/back_office_tabs/index.md). However, form submissions may require stricter access control than the form itself, for example, to conform with GDPR regulations. To tackle this, you must separate the permissions by introducing a dedicated policy that manages access to form submission: - define a custom policy: `form/read_submissions` - enforce the policy on the PHP API level - enforce the policy in the back office With this setup, users with `content/read` permission can view the form, but cannot see the **Submissions** tab, while users with `form/read_submissions` can access the submissions, export and manage submitted data (depending on other permissions). > **Note: Implementation notes** > > - This implementation uses service decoration and extends internal classes. > - Some internal methods are not publicly reusable, which may require additional calls, for example, `gateway->loadById($id)` or minor workarounds. > - When upgrading, review these customizations to ensure compatibility with internal API changes. ### Define custom policy First, create the `FormPolicyProvider.php` policy provider that registers the new `form` module and the `read_submissions` function by injecting the custom permission into the configuration tree: ```php addConfig([ 'form' => [ 'read_submissions' => null, ], ]); } public static function getTranslationMessages(): array { return [ (new Message('role.policy.form', 'forms'))->setDesc('Forms'), (new Message('role.policy.form.all_functions', 'forms'))->setDesc('Forms / All functions'), (new Message('role.policy.form.read_submissions', 'forms'))->setDesc('Forms / Read submissions'), ]; } } ``` Next, extract the [translations](#translations) to the `translations/forms.en.xlf` file. Then, register the provider in the Kernel by overriding the `build()` method. Unlike standard Symfony runtime services, policy providers must be registered explicitly in the application kernel, because they are consumed during the container compilation phase. ```php getExtension('ibexa'); // Add the policy provider, you can register multiple providers by calling the method repeatedly $ibexaExtension->addPolicyProvider(new FormPolicyProvider()); } } ``` Then, add a service definition to `config/services.yaml`: ```yaml services: # … App\Security\FormPolicyProvider: tags: - { name: ibexa.permissions.limitation_type } ``` Finally, add the policy definition in `src/Resources/config/policies.yaml`: ```yaml form: read_submissions: ~ ``` This way, after you clean the cache, the new policy becomes available when you [edit the policies assigned to a Role](https://doc.ibexa.co/projects/userguide/en/6.0/permission_management/work_with_permissions/). ### Secure access on PHP API level To enforce the policy on the PHP API level, decorate the form submission service to enforce permission checks. In `src/Security`, create the `FormSubmissionServiceDecorator.php` file: ```php innerService->create($content, $languageCode, $form, $data); } public function loadById(int $id): FormSubmission { $submissions = $this->gateway->loadById($id); // First manual data fetch if (empty($submissions)) { throw new NotFoundException('FormSubmission', $id); } $content = $this->contentService->loadContent($submissions[0]['content_id']); if (!$this->permissionResolver->canUser('form', 'read_submissions', $content)) { throw new UnauthorizedException('form', 'read_submissions', ['contentId' => $content->getId()]); // Permission check } return $this->innerService->loadById($id); // Second data fetch through inner service } // The same permission check pattern is repeated in the methods below public function delete(FormSubmission $submission): void { $submissionId = $submission->getId(); $submissions = $this->gateway->loadById($submissionId); if (empty($submissions)) { throw new NotFoundException('FormSubmission', $submissionId); } $content = $this->contentService->loadContent($submissions[0]['content_id']); if (!$this->permissionResolver->canUser('form', 'read_submissions', $content)) { throw new UnauthorizedException('form', 'read_submissions', ['contentId' => $content->getId()]); } $this->innerService->delete($submission); } public function loadByContent(ContentInfo $content, ?string $languageCode = null, int $offset = 0, int $limit = 25): FormSubmissionList { if (!$this->permissionResolver->canUser('form', 'read_submissions', $content)) { throw new UnauthorizedException('form', 'read_submissions', ['contentId' => $content->getId()]); } return $this->innerService->loadByContent($content, $languageCode, $offset, $limit); } public function loadAllByContentForExport(ContentInfo $content, ?string $languageCode = null): array { if (!$this->permissionResolver->canUser('form', 'read_submissions', $content)) { throw new UnauthorizedException('form', 'read_submissions', ['contentId' => $content->getId()]); } return $this->innerService->loadAllByContentForExport($content, $languageCode); } public function loadHeaders(ContentInfo $content, ?string $languageCode = null): array { if (!$this->permissionResolver->canUser('form', 'read_submissions', $content)) { throw new UnauthorizedException('form', 'read_submissions', ['contentId' => $content->getId()]); } return $this->innerService->loadHeaders($content, $languageCode); } public function getCount(ContentInfo $content, ?string $languageCode = null): int { if (!$this->permissionResolver->canUser('form', 'read_submissions', $content)) { throw new UnauthorizedException('form', 'read_submissions', ['contentId' => $content->getId()]); } return $this->innerService->getCount($content, $languageCode); } } ``` > **Note: Duplicate method calls** > > To perform a permission check for `$content`, it is fetched by `gateway->loadById($id)`. After permission is checked, `loadById($id)` is called again to prevent having to copy private method implementations into the decorator. Then, add a service definition to `config/services.yaml`: ```yaml services: # … App\Security\FormSubmissionServiceDecorator: decorates: Ibexa\FormBuilder\FormSubmission\FormSubmissionService arguments: $innerService: '@App\Security\FormSubmissionServiceDecorator.inner' ``` This way, users can't access the submission data unless they have the `form/read_submissions` policy added to their role. ### Secure back office access To enforce the policy in the back office, decorate the **Submissions** tab to hide it when the user lacks permission. In `src/Security`, create the `FormSubmissionsTabDecorator.php` file: ```php innerTab->getIdentifier(); } #[\Override] public function getName(): string { return $this->innerTab->getName(); } #[\Override] public function renderView(array $parameters): string { return $this->innerTab->renderView($parameters); } #[\Override] public function evaluate(array $parameters): bool { /** @var \Ibexa\Contracts\Core\Repository\Values\Content\Content $content */ $content = $parameters['content']; return $this->innerTab->evaluate($parameters) && $this->permissionResolver->canUser('form', 'read_submissions', $content); } #[\Override] public function getOrder(): int { return $this->innerTab->getOrder(); } } ``` Then, add a service definition to `config/services.yaml`: ```yaml services: # … App\Security\FormSubmissionsTabDecorator: parent: Ibexa\FormBuilder\Tab\LocationView\SubmissionsTab decorates: 'Ibexa\FormBuilder\Tab\LocationView\SubmissionsTab' arguments: $innerTab: '@.inner' ``` This way, users can't view the **Submissions** tab unless they have the `form/read_submissions` policy added to their role. # Users # Users > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). Users in Cohesivo refer to all kinds of user accounts, such as administrators, editors, managers or shop customers. Users in Cohesivo refer to all kinds of user accounts: administrators, editors, managers, or shop customers. All such user accounts have the same underlying mechanism and enable you to control access to the application, both the back office and the website front, by using the [permission system](https://doc.ibexa.co/en/saas/permissions/permissions/index.md). ## Invite and manage users - [User management product guide](https://ez-systems-developer-documentation--3381.com.readthedocs.build/en/3381/users/user_management_guide/): Find out what's user management and check what functions Cohesivo offers in this area to effectively manage the digital ecosystem. - [Inviting users](https://ez-systems-developer-documentation--3381.com.readthedocs.build/en/3381/users/invitations/): Manage user invitations to create an account in the frontend or the back office. - [Register new users](https://ez-systems-developer-documentation--3381.com.readthedocs.build/en/3381/users/user_registration/): Register new users. - [Update basic user data from CLI](https://ez-systems-developer-documentation--3381.com.readthedocs.build/en/3381/users/update_basic_user_data/): Update basic user account data from the console. ## Authenticate users - [Login methods](https://ez-systems-developer-documentation--3381.com.readthedocs.build/en/3381/users/login_methods/): Set up user login methods. - [Passwords](https://ez-systems-developer-documentation--3381.com.readthedocs.build/en/3381/users/passwords/): Set up user password rules. - [User authentication](https://ez-systems-developer-documentation--3381.com.readthedocs.build/en/3381/users/user_authentication/): Customize user authentication. - [OAuth client](https://ez-systems-developer-documentation--3381.com.readthedocs.build/en/3381/users/oauth_client/): Allow users to log into Cohesivo through external OAuth2 authorization servers. - [OAuth Server](https://ez-systems-developer-documentation--3381.com.readthedocs.build/en/3381/users/oauth_server/): Other applications can authenticate Cohesivo users through OAuth2 protocol then access to their resources on the platform. ## Group users - [Customer groups](https://ez-systems-developer-documentation--3381.com.readthedocs.build/en/3381/users/customer_groups/): Assigning users to customer groups allows defining user-specific pricing rules. - [Segment API](https://ez-systems-developer-documentation--3381.com.readthedocs.build/en/3381/users/segment_api/): You can use PHP API to get segment information, create and manage segments, and assign users to them. # User management product guide > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). Find out what's user management and check what functions Cohesivo offers in this area to effectively manage the digital ecosystem. User management is a fundamental aspect of any system. Cohesivo offers a comprehensive and feature-rich user management system that allows organizations to efficiently manage their digital ecosystem. ## What is user management User management refers to the process of granting, configuring, and controlling access for users by administrators. This encompasses the creation of user accounts, assigning roles and permissions, setting authentication methods, and managing user-related data. ## Availability User management is available in all Cohesivo versions. ## How does user management work Cohesivo simplifies user management with an intuitive and powerful system of accounts, roles, permissions, groups, and segments. You can find all user groups and users in the **Admin** panel by selecting **Users**. Here, you can manage users, their relations, roles, and policies. ![User's section](https://doc.ibexa.co/en/saas/users/img/users_section.png) Here's how it works: - User accounts - create and manage user accounts. This includes capturing user information, such as name, email, and profile details. - Roles and permissions - define roles and assign permissions to them. This ensures that users have appropriate access to content and functionalities. Roles can be customized to match the organization's specific needs. - Authentication methods - enable multiple authentication methods, including traditional username and password, OAuth, and external service logins. This flexibility allows organizations to adapt to various user authentication requirements. - User segmentation - segment users based on criteria such as demographics, behavior, or preferences. This segmentation enables personalized content delivery and targeted marketing. - Invitations - invite users to join a platform streamlining an onboarding process, sending invitations for exclusive content or events. - Customer groups - organize users into customer groups, which helps in delivering tailored experiences and content to specific segments. ![User management](https://doc.ibexa.co/en/saas/users/img/user_management.png) ## Capabilities The detailed capabilities of Ibexa user management, which provide organizations with the tools they need to deliver personalized, secure, and efficient user experiences while ensuring that user access and content delivery align with their business goals and strategies. ### User roles and permissions Ibexa allows you to define custom user [roles with granular permissions](https://doc.ibexa.co/en/saas/permissions/permission_overview/index.md), ensuring that users have access to only the specific parts of the system they need. Furthermore, you can create user groups to simplify Permission management. Assign multiple users to a group to ensure consistency and ease of access control. This helps maintain effortless security and control. To help you understand further the role each element serves, here's a brief summary: - Role - represents a collection of Permissions that can be assigned to users or user groups. Roles streamline permission management by grouping related Permissions together. - Permission - defines a specific action or access level that can be granted or denied within the system. - Policy - is a set of rules or conditions that determine under what circumstances a specific permission is granted or denied by applying limitations. Policies allow for fine-grained control of access based on various factors, such as user attributes or system states. ### Custom policies [Tailor user access control](https://doc.ibexa.co/en/saas/permissions/custom_policies/index.md) to your unique requirements by using custom policies. Define complex rules and access criteria for different users or groups. ### Limitations [Implement limitations](https://doc.ibexa.co/en/saas/permissions/limitations/index.md) on user actions based on specific criteria, such as time-based restrictions or geographic locations. ### Authentication methods Ibexa offers flexibility in authentication methods to cater to different user bases and security requirements. ![Log in via Google](https://doc.ibexa.co/en/saas/users/img/log_in_via_google.png) Available options: - [Username and password](https://doc.ibexa.co/en/saas/users/passwords/index.md) - ideal for most users, this traditional method offers a secure login process with username and password. - [OAuth client](https://doc.ibexa.co/en/saas/users/oauth_client/index.md) - integrating OAuth authentication allows users to log in using their existing social media credentials (like Google, Facebook, and Twitter), or the enterprise's system (like Active Directory or LDAP). - [OAuth server](https://doc.ibexa.co/en/saas/users/oauth_server/index.md) - client applications (such as mobile apps) can authenticate a user by using the platform's login screen, then access resources. ### Invitations The [invitation system](https://doc.ibexa.co/en/saas/users/invitations/index.md) streamlines user onboarding and engagement. Track the status of invitations, including when they were sent, whether they were accepted, and the actions taken by users who accepted them. ![Invitations](https://doc.ibexa.co/en/saas/users/img/users_invitation.png) ### User segmentation and recommendations Ibexa's segmentation and recommendations features allow organizations to deliver customized user experiences. Track user behavior, such as page views, search queries, and interactions, to create segments and segment groups for users who share similar behaviors. ![Segment groups](https://doc.ibexa.co/en/saas/administration/img/admin_panel_segment_groups.png) Possible uses: - Demographics - segment users based on demographic data such as age, location, and gender to personalize content, promotions, and recommendations. - Behavior - tailor content based on user behavior, such as frequent content consumption, shopping patterns, or search history, ensuring users see what they're interested in. - Preferences - utilize user preferences to offer a customized experience, from language preferences to content type preferences. ### Customer groups The customer group functionality allows for targeted content delivery and service offerings. Set specific permissions for customer groups to control who can access and edit certain content to get respective recommendations. Possible uses: - Product recommendations - create customer groups based on product preferences and offer tailored product recommendations. - Content access control - restrict access to premium or specialized content to specific customer groups, such as paid subscribers or loyal customers. ## Benefits ### Improved user experience With role-based access control and personalized content, users have a more engaging and relevant experience on your platform. ### Enhanced security The flexible authentication methods and permission management help safeguard sensitive data and maintain security. With the ability to define and manage user roles and permissions, clients can ensure that sensitive data and actions are protected. User management helps prevent unauthorized access. ### Efficient user onboarding Invitations and account creation streamline the process of onboarding new users. ### Targeted marketing Customer groups and user segmentation capabilities allow for targeted and effective marketing campaigns. ### Content governance Clients can enforce content governance by controlling who can edit and publish content. This ensures quality and consistency in their digital properties. ### Content relevance By delivering content that resonates with different user segments, clients can increase user engagement and retention. ### Customizability Clients can adapt the user management system to their unique needs. Custom policies and limitations enable tailored solutions that align with their specific use cases. # Inviting users > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). Manage user invitations to create an account in the frontend or the back office. Cohesivo allows you to create and send invitations to create an account in the frontend as a customer, the back office as an employee, or the Corporate Portal as an organisation member. You can send invitations to individual users or in bulk. ## Roles and policies To invite other members to the site or the back office, a user needs to have the `User:Invite` permission added to their role. You can limit the ability to invite other members to specific user groups, such as Editors, or to the specific roles within the group, for example: Admin, Buyer. ## Creating and sending invitations Invitations are created with [InvitationService](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-User-Invitation-InvitationService.html), but sending them requires additional setup. Cohesivo provides you with `Ibexa\User\Invitation\MailSender` implementation of `InvitationSender` interface for sending invitations via email. If you want to send invitations through different channels, you need to create a custom setup. ## Invitation and registration form templates ### Semantic configuration To set up custom templates for invitation or registration forms, create a template file and inform the system, through configuration, when to use this template. You might also set a SiteAccess under `scope`, to which the new user is invited. If the SiteAccess isn't set, it falls back to the default `site` value. For example, use the following [configuration](https://doc.ibexa.co/en/saas/administration/configuration/configuration/#configuration-files): ```yaml ibexa: system: : user_invitation: hash_expiration_time: P7D templates: mail: "@@App/invitation/mail.html.twig" ``` Here, you can specify which template should be used for the invitation mail, and what should be the expiration time for the invitation link included in that mail. If a user doesn't click the invitation link sent to them in time, you can refresh the invitation. Refresh resets the time limit and changes the hash in the invitation link. You can find more registration related templates in [Register new users documentation](https://doc.ibexa.co/en/saas/users/user_registration/#other-user-management-templates). # Register new users > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). Register new users. You can allow your users to create accounts by using the `/register` route. This route leads to a registration form that, when filled in, creates a new user content item in the repository. To give your users a possibility to register themselves, follow the instructions on [enabling account registration](https://doc.ibexa.co/en/saas/tutorials/beginner_tutorial/8_enable_account_registration/index.md). ## User types There are two user types defined: `users` and `customers`. `users` are back office users that are involved in creating the page such as editors, and `customers` are frontend users. To decide where the user should be registered to, you need to specify their user type under the `ibexa.system..user_type_identifier` [configuration key](https://doc.ibexa.co/en/saas/administration/configuration/configuration/#configuration-files). ```yaml ibexa: system: : user_registration: user_type_identifier: user ``` ## User groups By default, new users generated in this way are placed in the Guest accounts group. You can select a different default group in the following section of configuration: ```yaml ibexa: system: default: user_registration: group_remote_id: ``` ## Registration form field configuration To modify the registration form template, add or remove fields under the `allowed_field_definitions_identifiers` [configuration key](https://doc.ibexa.co/en/saas/administration/configuration/configuration/#configuration-files): ```yaml ibexa: system: : user_registration: user_type_identifier: user form: allowed_field_definitions_identifiers: - first_name - last_name - user_account ``` ## Other user management templates You can also modify form templates in the following way: ### Changing user password ```yaml ibexa: system: : user_change_password: templates: form: ``` ### Password recovery forms ```yaml ibexa.site_access.config..user_forgot_password.templates.form ibexa.site_access.config..user_forgot_password_success.templates.form ibexa.site_access.config..user_forgot_password_login.templates.form ibexa.site_access.config..user_forgot_password.templates.mail ``` ### Resetting password ```yaml ibexa.site_access.config..user_reset_password.templates.form ibexa.site_access.config..user_reset_password.templates.invalid_link ibexa.site_access.config..user_reset_password.templates.success ``` ### User settings ```yaml ibexa.site_access.config..user_settings.templates.list ibexa.site_access.config..user_settings.templates.update ``` ### Changing registration form templates To change the registration form template, follow the instructions in [Invitation and registration form templates](https://doc.ibexa.co/en/saas/users/invitations/#invitation-and-registration-form-templates). # Update basic user data from CLI > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). Update basic user account data from the console. Multiple user management scenarios may result in having to update basic user account data, such as user status, the password, or email. Especially, you may need to revoke user access by disabling the account when offboarding an employee, or change the user's forgotten password. You can do it without accessing the Admin UI, by running a console command. You reference the user account by passing the user login. ## Disable or enable user account Disable the user account: ```bash php bin/console ibexa:user:update-user --disable ``` For example: ```bash php bin/console ibexa:user:update-user --disable johndoe ``` Enable the user account: ```bash php bin/console ibexa:user:update-user --enable ``` ## Change password Change the password associated with the user account: ```bash php bin/console ibexa:user:update-user --password ``` After you run the command, enter the new password when prompted. The command runs in silent mode and inputs are not echoed. For more information about changing and revoking passwords, for example, when a security breach occurs, see [Passwords](https://doc.ibexa.co/en/saas/users/passwords/#revoking-passwords). ## Change email address Change the email address associated with the user account: ```bash php bin/console ibexa:user:update-user --email= ``` # Login methods > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). Set up user login methods. Two login methods are available: with user name or with email. Providers for these two methods are `ibexa.security.user_provider.username` and `ibexa.security.user_provider.email`. You can configure which method is allowed under the `security` [configuration key](https://doc.ibexa.co/en/saas/administration/configuration/configuration/#configuration-files): ```yaml security: providers: ibexa: chain: providers: [ibexa_username, ibexa_email] ibexa_username: id: ibexa.security.user_provider.username ibexa_email: id: ibexa.security.user_provider.email firewalls: #... ibexa_front: # ... provider: ibexa ``` You can customize per user field whether the email address used as a login method must be unique or not. To check that all existing user accounts have unique emails, run the `ibexa:user:audit-database` command. It lists all user accounts with duplicate emails. > **Caution: Caution** > > Because logging in with email was not available until version v3.0, you can come across issues if you use the option on an existing database. > > This may happen if more than one account uses the same email address. Login through the user name is still available. > > To resolve the issues, run `ibexa:user:audit-database` and manually modify accounts that have duplicate emails. ## Login rules You can set the rules for allowed user names in the back office per user field. The rules are set by using regular expressions. For example, to ensure that user names can only contain lowercase letters, set `[a-z]+$` as **Username pattern**: ![Setting a user name pattern](https://doc.ibexa.co/en/saas/users/img/username_pattern.png) To check that all existing user accounts have names that fit the current pattern, run the `ibexa:user:audit-database` command. It checks all user accounts in the database and lists those that don't fit the pattern. # Passwords > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). Set up user password rules. ## Changing and recovering passwords The user may request to change their password, or may forget it and ask to have it reset. To change password, the user must have the `user/password` permission. When the user requests a reset of a forgotten password, an email is sent to them with a token. It allows them to create a new password. For information about how to create and configure the template, see [Add forgot password option](https://doc.ibexa.co/en/saas/templating/layout/add_forgot_password_option/index.md) The template for this email is located in `Resources/views/forgot_password/mail/forgot_user_password.html.twig` in `ibexa/user`. You can [customize it according to your needs](https://doc.ibexa.co/en/saas/templating/layout/add_login_form/#customize-login-form). The validity of the password recovery token can be set by using the `ibexa.system..security.token_interval_spec` parameter. By default, it's set to `PT1H` (one hour). ## Revoking passwords In case of a security situation such as a data leakage, you may need to force users to change their passwords. You can do it with the help of the `ibexa:user:expire-password` command, which revokes the passwords for specific users, user groups, or users belonging to the chosen content type. To select which users to revoke passwords for, use one of the following options with the command: - `--user-id|-u` - the ID of the user. Accepts multiple user IDs - `--user-group-id|-ug` - the ID of the user group. Accepts multiple group IDs - `--user-content-type-identifier|-ct` - the identifier of the user content type. Accepts multiple content types You can use the following additional options with the command: - `--force|-f` - commits the change, otherwise the command only performs a dry run - `--iteration-count|-c` - defines how many users are fetched at once. Lowering this value helps with memory issues - `--password-ttl|-t` - number of days after which new passwords expire. Used when the command enables password expiration for user content types that don't use it yet. For example, to revoke the passwords of all users of the `user` content type, run: ```bash php bin/console ibexa:user:expire-password --user-content-type-identifier=user --force ``` To perform a dry run (without saving the results) of revoking passwords of all users from user group 13, run: ```bash php bin/console ibexa:user:expire-password --user-group-id=13 ``` ## Password rules You can customize the password policy in your project. Each password setting is customizable per user field type. You can change the [password attributes](#password-attributes) or [password expiration settings](#password-expiration), and determine the rules for [repeating passwords](#repeating-passwords). To access the password settings: 1. In the back office, go to **Content** -> **Content types**. 2. In the **Content type groups** table, click **Users**. 3. Edit the **User** content type. 4. In the **Field definitions** list, view the settings for **User account (ibexa_user)**. > **Tip: Tip** > > There can be other content types that function as users, beyond the built-in user content type. For details, see [User Identifiers](https://doc.ibexa.co/en/saas/administration/configuration/repository_configuration/#user-identifiers). ## Password attributes In the **User account (ibexa_user)** Field definition, you can determine if the password must contain at least: - One uppercase letter - One lowercase letter - One number - One non-alphanumeric character You can also set the minimum password length. ## Password expiration In the **User account (ibexa_user)** field definition, you can set password expiration rules, which forces users to change their passwords periodically. ![Password expiry settings](https://doc.ibexa.co/en/saas/users/img/password_expiry.png) You can also decide when the user is notified that they need to change their password. The notification is displayed in the back office after login and in the user content item's preview. ## Repeating passwords You can set a rule that the password cannot be reused. You set it for the user content type in the **User account (ibexa_user)** field type's settings. When this is set, the user cannot type in the same password when it expires. It has to be changed to a new one. This only checks the new password against the current one. A password that has been used before can be used again. This rule is valid by default when password expiration is set. ## Breached passwords You can set a rule that prevents using passwords which have been exposed in a public breach. To do this, in the **User account (ibexa_user)** field definition, select "Password must not be contained in a public breach". ![Protection against using breached passwords](https://doc.ibexa.co/en/saas/users/img/password_breached.png) This rule checks the password against known password dumps by using the API. It doesn't check existing passwords, so it doesn't block login for anyone. It applies only to new passwords when users change them. > **Note: Note** > > The password itself isn't sent to the API, which makes this check secure. > > For more information on how that is possible, see [Validating Leaked Passwords with k-Anonymity](https://blog.cloudflare.com/validating-leaked-passwords-with-k-anonymity/). # User authentication > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). Customize user authentication. ## Authenticate user with multiple user providers Symfony provides native support for [multiple user providers](https://symfony.com/doc/7.4/security/user_providers.html). This makes it easier to integrate any kind of login handlers, including SSO and existing third party bundles (for example, [FR3DLdapBundle](https://github.com/Maks3w/FR3DLdapBundle), [HWIOauthBundle](https://github.com/hwi/HWIOAuthBundle), [FOSUserBundle](https://github.com/FriendsOfSymfony/FOSUserBundle), or [BeSimpleSsoAuthBundle](https://github.com/BeSimple/BeSimpleSsoAuthBundle)). However, to be able to use *external* user providers with Cohesivo, a valid Ibexa user needs to be injected into the repository. This is mainly for the kernel to be able to manage content-related permissions (but not limited to this). Depending on your context, you either want to create and return an Ibexa user, or return an existing user, even a generic one. Whenever a user is matched and authenticated, Symfony initiates an `AuthenticationTokenCreatedEvent`. Every service listening to this event receives an object containing the original security token, which holds the matched user, and a [passport](https://symfony.com/doc/7.4/security/custom_authenticator.html#security-passports). Then, it's up to a listener to retrieve an Ibexa user from the repository. This Ibexa user can be: - embedded into `Ibexa\Core\MVC\Symfony\Security\User` while forgetting about the original user - wrapped into `Ibexa\Core\MVC\Symfony\Security\UserWrapped` with the original user if needed Finally, the user is assigned back into the event's token for the rest of the process. ### User mapping example The following example uses the [memory user provider](https://symfony.com/doc/7.4/security/user_providers.html#memory-user-provider), maps memory user to Ibexa repository user, and [chains](https://symfony.com/doc/7.4/security/user_providers.html#chain-user-provider) with the Ibexa user provider to be able to use both. It's possible to customize the user class used by extending `Ibexa\Core\MVC\Symfony\Security\EventListener\SecurityListener` service, which defaults to `Ibexa\Core\MVC\Symfony\Security\EventListener\SecurityListener`. You can override `getUser()` to return whatever user class you want, as long as it implements `Ibexa\Core\MVC\Symfony\Security\UserInterface`. The following is an example of using the in-memory user provider: ```yaml # config/packages/security.yaml security: providers: # Chaining in_memory and ibexa user providers chain_provider: chain: providers: [in_memory, ibexa] ibexa: id: ibexa.security.user_provider in_memory: memory: users: # You will then be able to login with username "user" and password "userpass" user: { password: userpass, roles: [ 'ROLE_USER' ] } # The "in memory" provider requires an encoder for Symfony\Component\Security\Core\User\User encoders: Symfony\Component\Security\Core\User\User: plaintext ``` ### Implement the listener In the `config/services.yaml` file: ```yaml services: App\EventListener\InteractiveLoginListener: arguments: ['@ibexa.api.service.user'] tags: - { name: kernel.event_subscriber } ``` Don't mix `MVCEvents::INTERACTIVE_LOGIN` event (specific to Cohesivo) and `SecurityEvents::INTERACTIVE_LOGIN` event (fired by Symfony security component). ```php $userMap */ public function __construct( private readonly ConfigResolverInterface $configResolver, private readonly UserService $userService, private readonly array $userMap = [], ) { } public static function getSubscribedEvents(): array { return [ AuthenticationTokenCreatedEvent::class => ['onAuthenticationTokenCreated', 11], ]; } public function onAuthenticationTokenCreated(AuthenticationTokenCreatedEvent $event): void { $token = $event->getAuthenticatedToken(); $tokenUser = $token->getUser(); if (!$tokenUser instanceof InMemoryUser) { return; } $userIdentifier = $token->getUserIdentifier(); $ibexaUser = null; if (array_key_exists($userIdentifier, $this->userMap)) { $ibexaUser = $this->userService->loadUserByLogin($this->userMap[$userIdentifier]); } if (null === $ibexaUser) { $anonymousUserId = (int)$this->configResolver->getParameter('anonymous_user_id'); $ibexaUser = $this->userService->loadUser($anonymousUserId); } $token->setUser(new UserWrapped($tokenUser, $ibexaUser)); } } ``` In `config/packages/security.yaml`, add the `memory` and `chain` user providers, store some in-memory users with their passwords in plain text and a basic role, set a `plaintext` password encoder for the `memory` provider's `InMemoryUser`, and configure the firewall to use the `chain` provider: ```yaml security: password_hashers: # The in-memory provider requires an encoder Symfony\Component\Security\Core\User\InMemoryUser: plaintext Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface: 'auto' # https://symfony.com/doc/current/security.html#b-configuring-how-users-are-loaded providers: in_memory: memory: users: from_memory_user: { password: from_memory_pass, roles: [ 'ROLE_USER' ] } # Mapped to `generic_customer` user from_memory_forgotten: { password: from_memory_anonym, roles: [ 'ROLE_USER' ] } # Not mapped so `anonymous` user is loaded from_memory_admin: { password: from_memory_publish, roles: [ 'ROLE_USER' ] } # Mapped to `admin` user ibexa: id: ibexa.security.user_provider # Chaining in_memory and ibexa user providers chained: chain: providers: [ in_memory, ibexa ] firewalls: # … ibexa_front: pattern: ^/ provider: chained user_checker: Ibexa\Core\MVC\Symfony\Security\UserChecker context: ibexa form_login: enable_csrf: true login_path: login check_path: login_check custom_authenticators: - Ibexa\PageBuilder\Security\EditorialMode\FragmentAuthenticator entry_point: form_login logout: path: logout ``` In the `config/services.yaml` file, declare the subscriber as a service to pass your user map. Since it implements the `EventSubscriberInterface`, it's automatically tagged as a `kernel.event_subscriber`. The config resolver and user service injections are auto-wired automatically. ```yaml services: App\EventSubscriber\AuthenticationTokenCreatedSubscriber: arguments: $userMap: from_memory_user: generic_customer from_memory_admin: admin ``` You can list the subscribers with the following command to check their order: ```bash php bin/console debug:event-dispatcher AuthenticationTokenCreatedEvent ``` Notice that the example subscriber priority is `11` so it's executed before the `Ibexa\Core\MVC\Symfony\Security\Authentication\EventSubscriber\OnAuthenticationTokenCreatedRepositoryUserSubscriber` which set the Ibexa user as the current user. From the back office, create the mapped users. For this example, create a new user with the login `generic_customer` and a random password so the mapping works correctly. This account can belong to either the **Customers** or the **Anonymous users** group. You can now log in with an in-memory user. In the Symfony debug toolbar, you should see the in-memory user as this example uses `UserWrapped`. # OAuth client > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). Allow users to log into Cohesivo through external OAuth2 authorization servers. You can use OAuth2 to securely authenticate users with external Authorization Servers. ![OAuth2 Client](https://doc.ibexa.co/en/saas/users/img/oauth2-client.png) Cohesivo uses an integration with [`knpuniversity/oauth2-client-bundle`](https://github.com/knpuniversity/oauth2-client-bundle) to provide OAuth2 authentication. ## Configure OAuth2 client ### Configure connection to Authorization Server Details of the configuration depend on the OAuth2 Authorization Server that you want to use. For sample configurations for different providers, see [`knpuniversity/oauth2-client-bundle` configuration](https://github.com/knpuniversity/oauth2-client-bundle#configuration). Some client types require additional packages. Missing package is indicated in an error message. For example, the following configuration creates a `google` client for Google OAuth2 Authorization Server to log users in. Two environment variables, `OAUTH_GOOGLE_CLIENT_ID` and `OAUTH_GOOGLE_CLIENT_SECRET`, correspond to [the set-up on Google side](https://support.google.com/cloud/answer/15549257). ```yaml knpu_oauth2_client: clients: # Configure your clients as described here: https://github.com/knpuniversity/oauth2-client-bundle#configuration google: type: google client_id: '%env(OAUTH_GOOGLE_CLIENT_ID)%' client_secret: '%env(OAUTH_GOOGLE_CLIENT_SECRET)%' redirect_route: ibexa.oauth2.check redirect_params: identifier: google ``` To use the `google` client type, you need to install the following package: ```bash composer require league/oauth2-google ``` ### Enable OAuth2 client The client needs to be a part of the [SiteAccess scope](https://doc.ibexa.co/en/saas/multisite/multisite_configuration/#scope). In the following example, the OAuth2 client `google` is enabled for the `admin` SiteAccess: ```yaml ibexa: system: admin: oauth2: enabled: true clients: ['google'] ``` ## Configure firewall In `config/packages/security.yaml`, enable the `ibexa_oauth2_connect` firewall and replace the `ibexa_front` firewall with the `ibexa_oauth2_front` one. ```yaml security: #… firewalls: #… # Uncomment ibexa_oauth2_connect, ibexa_oauth2_front rules and comment ibexa_front firewall # to enable OAuth2 authentication ibexa_oauth2_connect: pattern: /oauth2/connect/* security: false ibexa_oauth2_front: pattern: ^/ provider: ibexa user_checker: Ibexa\Core\MVC\Symfony\Security\UserChecker custom_authenticators: - Ibexa\Bundle\OAuth2Client\Security\Authenticator\OAuth2Authenticator - Ibexa\PageBuilder\Security\EditorialMode\FragmentAuthenticator entry_point: Ibexa\Bundle\OAuth2Client\Security\Authenticator\OAuth2Authenticator context: ibexa form_login: enable_csrf: true logout: ~ # ibexa_front: # pattern: ^/ # provider: ibexa # user_checker: Ibexa\Core\MVC\Symfony\Security\UserChecker # context: ibexa # form_login: # enable_csrf: true # login_path: login # check_path: login_check # custom_authenticators: # - Ibexa\PageBuilder\Security\EditorialMode\FragmentAuthenticator # entry_point: form_login # logout: # path: logout ``` The `custom_authenticators` setting specifies the [custom authenticators](https://symfony.com/doc/7.4/security/custom_authenticator.html) to be used. By adding the `Ibexa\Bundle\OAuth2Client\Security\Authenticator\OAuth2Authenticator` authenticator you add a possibility to use OAuth2 on those routes. ## Resource owner mappers Resource owner mappers map the data received from the OAuth2 authorization server to user information in the repository. Resource owner mappers must implement the [`Ibexa\Contracts\OAuth2Client\ResourceOwner\ResourceOwnerMapper`](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-OAuth2Client-ResourceOwner-ResourceOwnerMapper.html) interface. Four implementations of `ResourceOwnerMapper` are proposed by default: - `ResourceOwnerToExistingUserMapper` is the base class extended by the following mappers: - `ResourceOwnerIdToUserMapper` - loads a user (resource owner) based on the identifier, but doesn't create a new user. - `ResourceOwnerEmailToUserMapper` - loads a user (resource owner) based on the email, but doesn't create a new user. - `ResourceOwnerToExistingOrNewUserMapper` - checks whether the user exists and loads the data if it does. If not, creates a new user in the repository. To use `ResourceOwnerToExistingOrNewUserMapper`, you need to extend it in your custom mapper. > **Tip: OAuth user content type** > > When you implement your own mapper for external login, it's good practice to create a special user content type for users registered in this way. The users who register through an external service don't have a separate password in the system. Instead, they log in by their external service's password. > > To avoid issues with password restrictions in the built-in user content type, create a special content type (for example, "OAuth user"), without restrictions on the password. > > This new content type must also contain the user (`ibexa_user`) field. The following example shows how to create a Resource Owner mapper for the `google` client from previous examples. Create a resource owner mapper for Google login in `src/OAuth/GoogleResourceOwnerMapper.php`. The mapper extends `ResourceOwnerToExistingOrNewUserMapper`, which enables it to create a new user in the repository if the user doesn't exist yet. The mapper loads a user (line 40) or creates a new one (line 49), based on the information from `resourceOwner`, that's the OAuth2 authorization server. The new username is set with a `google:` prefix (lines 20, 91), to avoid conflicts with users registered in a regular way. ```php loadUserByIdentifier($this->getUsername($resourceOwner)); } /** * @param \League\OAuth2\Client\Provider\GoogleUser $resourceOwner */ protected function createUser( ResourceOwnerInterface $resourceOwner, UserProviderInterface $userProvider ): UserInterface { $userCreateStruct = $this->oauthUserService->newOAuth2UserCreateStruct( $this->getUsername($resourceOwner), $resourceOwner->getEmail(), $this->getMainLanguageCode(), $this->getOAuth2UserContentType($this->repository) ); $userCreateStruct->setField('first_name', $resourceOwner->getFirstName()); $userCreateStruct->setField('last_name', $resourceOwner->getLastName()); $parentGroups = []; if ($this->parentGroupRemoteId !== null) { $parentGroups[] = $this->userService->loadUserGroupByRemoteId($this->parentGroupRemoteId); } $this->userService->createUser($userCreateStruct, $parentGroups); return $userProvider->loadUserByIdentifier($this->getUsername($resourceOwner)); } private function getOAuth2UserContentType(Repository $repository): ?ContentType { if ($this->contentTypeIdentifier !== null) { $contentTypeService = $repository->getContentTypeService(); return $contentTypeService->loadContentTypeByIdentifier( $this->contentTypeIdentifier ); } return null; } private function getMainLanguageCode(): string { // Get first prioritized language for current scope return $this->languageResolver->getPrioritizedLanguages()[0]; } private function getUsername(GoogleUser $resourceOwner): string { return self::PROVIDER_PREFIX . $resourceOwner->getId(); } } ``` Configure the service by using the `ibexa.oauth2_client.resource_owner_mapper` tag to associate it with the `google` client: ```yaml services: #… App\OAuth\GoogleResourceOwnerMapper: tags: - { name: ibexa.oauth2_client.resource_owner_mapper, identifier: google } ``` ## Add login button After you have activated the OAuth2 client for the `admin` SiteAccess, you need to add a **Log in with Google** to the back office login form. Create the following template file in `templates/themes/admin/account/login/oauth2_login.html.twig`: ```html+twig ``` The OAuth connection URL Twig functions are `ibexa_oauth2_connect_path` and `ibexa_oauth2_connect_url`. Finally, add the template to the login form by using the `admin-ui-login-form-after` [Twig component group](https://doc.ibexa.co/en/saas/templating/components/index.md): ```yaml services: #… app.components.oauth2_login: parent: Ibexa\TwigComponents\Component\TemplateComponent arguments: $template: '@@ibexadesign/account/login/oauth2_login.html.twig' tags: - { name: ibexa.twig.component, group: admin-ui-login-form-after } ``` ![Log in to the back office with Google](https://doc.ibexa.co/en/saas/users/img/log_in_via_google.png) # OAuth Server > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). Other applications can authenticate Cohesivo users through OAuth2 protocol then access to their resources on the platform. Your Cohesivo instance can be used as an OAuth2 server, combining the roles of an Authorization Server and a Resource Server. Client applications, such as mobile apps, are able to authenticate a user, and then access this user's resources. ![OAuth2 Server](https://doc.ibexa.co/en/saas/users/img/oauth2-server.png) ## Server installation Cohesivo Oauth2 server package is `ibexa/oauth2-server` and isn't part of the default installation. You can install it with the following command: ```bash composer require ibexa/oauth2-server --with-all-dependencies ``` Add the tables needed by the bundle: **MySQL** ```bash php bin/console ibexa:doctrine:schema:dump-sql vendor/ibexa/oauth2-server/src/bundle/Resources/config/schema.yaml | mysql -u -p ``` **PostgreSQL** ```bash php bin/console ibexa:doctrine:schema:dump-sql --force-platform=postgres vendor/ibexa/oauth2-server/src/bundle/Resources/config/schema.yaml | psql ``` Then, in `config/bundles.php`, at the end of an array with a list of bundles, add the following two lines : ```php ['all' => true], League\Bundle\OAuth2ServerBundle\LeagueOAuth2ServerBundle::class => ['all' => true], ]; ``` ## Authorization Server configuration ### Keys To configure the server, you need private and public keys. For more information, see [Generating public and private keys](https://oauth2.thephpleague.com/installation/#generating-public-and-private-keys). You also need an encryption key. For more information, see [Generating encryption keys](https://oauth2.thephpleague.com/installation/#generating-encryption-keys). Set the following environment variables: ```bash OAUTH2_PUBLIC_KEY_PATH=/somewhere/safe/key.public OAUTH2_PRIVATE_KEY_PATH=/somewhere/safe/key.private OAUTH2_PRIVATE_KEY_PASSPHRASE=some_passphrase_or_empty OAUTH2_ENCRYPTION_KEY=1234567890123456789012345678901234567890 ``` ### Service, routes, and security configurations Uncomment the whole service configuration file: `config/packages/ibexa_oauth2_server.yaml`. Tweak the values if necessary. Uncomment the whole routes configuration file: `config/routes/ibexa_oauth2_server.yaml`. In `config/packages/security.yaml`, uncomment the following three lines under the `access_control` key: ```yaml security: #… # Uncomment authorize access control if you wish to use product as an OAuth2 Server access_control: - { path: ^/authorize/jwks$, roles: ~ } - { path: ^/authorize, roles: IS_AUTHENTICATED_REMEMBERED } ``` In `config/packages/security.yaml`, uncomment the three following lines under the `oauth2_token` key: ```yaml security: #… firewalls: #… # Uncomment oauth2_token firewall if you wish to use product as an OAuth2 Server. # Use oauth2 guard any other (for example ibexa_front) firewall you wish to be # exposed as OAuth2-available resource. Example: # guard: # authenticators: # - Ibexa\OAuth2Server\Security\Guard\OAuth2Authenticator oauth2_token: pattern: ^/token$ security: false ``` ## Resource Server configuration To allow resource routes to be accessible through OAuth authorization, enable OAuth2 integration for the `ibexa_rest` firewall by setting the `oauth2` property to true. ## Client ### Add a client You need the client redirect URIs to create a client. You also need to agree on an identifier and a secret with the client. There is only one `default` [scope](https://oauth.net/2/scope/). Use `league:oauth2-server:create-client` command to create a client. For example: ```bash php bin/console league:oauth2-server:create-client 'Example OAuth2 Client' example-oauth2-client 9876543210987654321098765432109876543210 --scope=default \ --redirect-uri=https://example.com/oauth2-callback ``` > **Note: Note** > > You can call `--redirect-uri` multiple times. Alternatively, you could add redirect URIs after you create a client, by using the `league:oauth2-server:update-client` command. For example: ```bash php bin/console league:oauth2-server:update-client example-oauth2-client \ --add-redirect-uri=https://example.com/another-oauth2-callback ``` Other commands let you list all the configured clients (`league:oauth2-server:list-clients`) or delete a client (`league:oauth2-server:delete-client`). > **Note: Note** > > For a list of all the commands that you can use maintain your clients, in a terminal, run `bin/console list league:oauth2-server`. To see usage details for each of the commands, run `bin/console help ` . > > For more information, see the package's [online documentation](https://github.com/thephpleague/oauth2-server-bundle/blob/1.x/docs/basic-setup.md). ### Information needed by the client Your OAuth2 client needs the following information to be able to use your OAuth server: - The URL of Cohesivo used as an OAuth server - The client identifier - The client secret - The scope (`default`) # Customer groups > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). Assigning users to customer groups allows defining user-specific pricing rules. You can assign users to different customer groups to enable [custom pricing](https://doc.ibexa.co/en/saas/product_catalog/prices/index.md). This enables you to give specific prices or price discounts (global or per product) to specific groups of users. For example, you can offer a 10% discount for all products in the catalog to users who belong to the Resellers customer group. > **Tip: Tip** > > Customer groups aren't the same as [user groups](https://doc.ibexa.co/en/saas/users/user_registration/#user-groups). User groups concern all users in the system and can be used, for example, to handle permissions. Customer groups refer specifically to the product catalog functionalities and enable handling prices. ## Enabling customer groups To enable the use of customer groups, you need to modify the user content type's definition by adding a [customer group field](https://doc.ibexa.co/en/saas/content_management/field_types/field_type_reference/customergroupfield/index.md). With this field you can add a user to any of the predefined customer groups. # Segment API > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). You can use PHP API to get segment information, create and manage segments, and assign users to them. Editions: Experience Segments enable you to profile the content displayed to specific users. To manage segments, use the `SegmentationService`. ## Getting segment information To load a segment group, use `SegmentationService::loadSegmentGroupByIdentifier()`. Get all segments assigned to the group with `SegmentationService::loadSegmentsAssignedToGroup()`: ```php $segmentGroup = $this->segmentationService->loadSegmentGroupByIdentifier('custom_group'); $segments = $this->segmentationService->loadSegmentsAssignedToGroup($segmentGroup); foreach ($segments as $segment) { $output->writeln('Segment identifier: ' . $segment->getIdentifier() . ', name: ' . $segment->getName()); } ``` Similarly, you can load a segment by using `SegmentationService::loadSegmentByIdentifier()`: ```php $segment = $this->segmentationService->loadSegmentByIdentifier('segment_1'); ``` ## Checking assignment You can check whether a user is assigned to a segment with `SegmentationService::isUserAssignedToSegment()`: ```php $output->writeln(( $this->segmentationService->isUserAssignedToSegment($user, $segment) ? 'The user is assigned to the segment.' : 'The user is not assigned to the segment.' )); ``` ## Assigning users To assign a user to a segment, use `SegmentationService::assignUserToSegment()`: ```php $this->segmentationService->assignUserToSegment($user, $segment); ``` ## Creating segments Each segment must be assigned to a segment group. To create a segment group, use `SegmentationService::createSegmentGroup()` and provide it with a `SegmentGroupCreateStruct`: ```php $segmentGroupCreateStruct = new SegmentGroupCreateStruct([ 'name' => 'Custom Group', 'identifier' => 'custom_group', 'createSegments' => [], ]); $newSegmentGroup = $this->segmentationService->createSegmentGroup($segmentGroupCreateStruct); ``` To add a segment, use `SegmentationService::createSegment()` and provide it with a `SegmentCreateStruct`, which takes an existing group as one of the parameters: ```php $segmentCreateStruct = new SegmentCreateStruct([ 'name' => 'Segment 1', 'identifier' => 'segment_1', 'group' => $newSegmentGroup, ]); $newSegment = $this->segmentationService->createSegment($segmentCreateStruct); ``` ## Updating segments To update a segment or a segment group, use `SegmentationService::updateSegment()` or `SegmentationService::updateSegmentGroup()` and provide it with `SegmentUpdateStruct` or `SegmentGroupUpdateStruct`. ## Deleting segments To delete a segment or a segment group, use `SegmentationService::removeSegment()` or `SegmentationService::removeSegmentGroup()`: ```php $this->segmentationService->removeSegmentGroup($segmentGroup); ``` # Recommendations # Raptor integration > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). Step-by-step activation procedure of setting up the Raptor connector. The [Raptor](https://www.raptorservices.com/) integration is an add-on that provides a seamless integration between Cohesivo and Raptor recommendation engine. Its primary goal is to enable editors and managers to deliver personalized experiences across digital channels, which helps increase conversion rates, drive sales, and improve user engagement. By combining content management capabilities with advanced recommendation features, the connector allows teams to build and manage personalized experiences across integrated tools. The connector ensures a smooth and unified integration layer, enabling: - event tracking through the tracking API - personalized delivery of content and product recommendations through the Recommendations API - flexible, SiteAccess-aware configuration This approach reduces integration complexity while providing a scalable foundation for personalization use cases across multiple sites and markets. To configure the integration with Raptor, follow a step-by-step procedure that allows you to activate the Raptor connector. Activation includes [configuration](https://doc.ibexa.co/en/saas/recommendations/raptor_integration/connector_installation_configuration/index.md), adding tracking scripts and events, and using [Page Builder](https://doc.ibexa.co/en/saas/content_management/pages/page_builder_guide/index.md) blocks. For more information about tracking, check the Raptor documentation: [Implementing tracking](https://content.raptorservices.com/help-center/data-management#implementing-tracking). - [Install and configure Raptor](https://ez-systems-developer-documentation--3381.com.readthedocs.build/en/3381/recommendations/raptor_integration/connector_installation_configuration/): To configure the Raptor integration, follow the step-by-step procedure described below. - [Tracking functions](https://ez-systems-developer-documentation--3381.com.readthedocs.build/en/3381/recommendations/raptor_integration/tracking_functions/): Integrate the tracking script to collect user interactions. - [Hybrid tracking](https://ez-systems-developer-documentation--3381.com.readthedocs.build/en/3381/recommendations/raptor_integration/hybrid_tracking/): Enable hybrid tracking to avoid ad blockers and proxy events through your server. - [Tracking with PHP API](https://ez-systems-developer-documentation--3381.com.readthedocs.build/en/3381/recommendations/raptor_integration/tracking_php_api/): Tracking with PHP API. - [Recommendation blocks in Page Builder](https://ez-systems-developer-documentation--3381.com.readthedocs.build/en/3381/recommendations/raptor_integration/recommendation_blocks/): Recommendation blocks in Page Builder - [Render recommendations](https://ez-systems-developer-documentation--3381.com.readthedocs.build/en/3381/recommendations/raptor_integration/custom_recommendation_rendering/): Use existing controllers to render recommendations outside the Page Builder. # Raptor integration product guide > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). Discover Raptor integration - an add-on focused on recommendations and tracking customer behaviors. Discover [Raptor](https://www.raptorservices.com/) integration - an add-on that is focused on recommendations and tracking customer behaviors. It includes the connector with tracking scripts and events that are used to track and analyze customer behaviors, and a set of Recommendation blocks. ## What is Raptor integration The [Raptor integration](https://doc.ibexa.co/en/saas/recommendations/raptor_integration/raptor_connector/index.md) provides a seamless integration between Cohesivo and the Raptor recommendation engine. Its primary goal is to enable editors and managers to deliver personalized experiences across digital channels, which helps to increase conversion rates, drive sales, and improve user engagement. By bringing content and recommendations together, the connector makes it easy to build and manage personalized experiences. It provides a seamless integration layer that supports: - event tracking of user interactions through the Tracking API - personalized delivery of content and product recommendations through the Recommendations API - flexible SiteAccess-aware configuration adapted to different sites and contexts This approach simplifies integration while supporting personalization across different sites and markets. ## Availability Raptor integration elements, such as tracking, Twig functions, and public API, are available in all supported Cohesivo editions starting from v5.0.7 version. Recommendation blocks provided in Page Builder, are available in Ibexa Experience and Ibexa Commerce editions. ## How does Raptor tracking work To start [tracking](https://content.raptorservices.com/help-center/introduction-to-tracking-documentation) user interactions, the tracking script needs to be added to the website’s layout. Tracking can be set up either on the client-side, server-side, or using hybrid mode, depending on how you want to capture and process the events. The tracking works differently depending on the mode you choose. In server-side mode, tracking happens on the server, handling all events without loading scripts in the browser. In client-side mode, it inserts script tags so tracking runs directly in the browser. In hybrid mode, the browser loads a first-party [shim]() that forwards tracking events to a same-origin proxy endpoint instead of the Raptor SaaS script, helping prevent ad blockers from blocking tracking. For more information, see [Hybrid tracking](https://doc.ibexa.co/en/saas/recommendations/raptor_integration/hybrid_tracking/index.md). You can switch between tracking modes at any time by changing the tracking type to fit your setup and needs. ## Capabilities ### Tracking Raptor [tracking functions](https://doc.ibexa.co/en/saas/recommendations/raptor_integration/tracking_functions/index.md) allow you to collect data about how users interact with your products and content. You can track product visits to better understand what users are viewing. Provided Twig functions simplify the implementation, allowing developers to quickly add tracking to templates without complex setup. This gives you the data you need to better understand user behavior, improve recommendations, and support personalization. ### Recommendation blocks The Raptor integration add-on provides a set of ready-to-use recommendation blocks that can be added directly in the [Page Builder](https://doc.ibexa.co/en/saas/content_management/pages/page_builder_guide/index.md). These blocks can be configured to adjust how they work and what they display. Content, Product, and Commerce recommendations can be placed on landing pages using these components. Editors can use these blocks to display tailored product recommendations, promote related content, and highlight items that are trending or recently viewed. Recommendation blocks are organized into dedicated categories, each grouping blocks based on the type of recommendation they provide: - **Recommendations: Content** - presents content recommendations: - [Content that has been seen along with the item category](https://doc.ibexa.co/projects/userguide/en/6.0/recommendations/raptor_integration/raptor_recommendation_blocks/#content-that-has-been-seen-along-with-the-item-category-block) - [Merchandising content sorted by personal preferences and popularity](https://doc.ibexa.co/projects/userguide/en/6.0/recommendations/raptor_integration/raptor_recommendation_blocks/#merchandising-content-sorted-by-personal-preferences-and-popularity) - [Most popular content](https://doc.ibexa.co/projects/userguide/en/6.0/recommendations/raptor_integration/raptor_recommendation_blocks/#most-popular-content-block) - [Other customers have also seen this content](https://doc.ibexa.co/projects/userguide/en/6.0/recommendations/raptor_integration/raptor_recommendation_blocks/#other-customers-have-also-seen-this-content-block) - [Personalized content recommendations](https://doc.ibexa.co/projects/userguide/en/6.0/recommendations/raptor_integration/raptor_recommendation_blocks/#personalized-content-recommendations-block) - [User’s content history](https://doc.ibexa.co/projects/userguide/en/6.0/recommendations/raptor_integration/raptor_recommendation_blocks/#users-content-history-block) - **Recommendations: Product** - displays product suggestions based on visitors’ browsing history: - [Items associated with the given Content](https://doc.ibexa.co/projects/userguide/en/6.0/recommendations/raptor_integration/raptor_recommendation_blocks/#items-associated-with-the-given-content-block) - [Items of Customized Feeds sorted by personal preferences and popularity or trendiness](https://doc.ibexa.co/projects/userguide/en/6.0/recommendations/raptor_integration/raptor_recommendation_blocks/#items-of-customized-feeds-sorted-by-personal-preferences-and-popularity-or-trendiness) - [Most popular products](https://doc.ibexa.co/projects/userguide/en/6.0/recommendations/raptor_integration/raptor_recommendation_blocks/#most-popular-products-block) - [Most popular products in category](https://doc.ibexa.co/projects/userguide/en/6.0/recommendations/raptor_integration/raptor_recommendation_blocks/#most-popular-products-in-category-block) - [Other customers have also seen](https://doc.ibexa.co/projects/userguide/en/6.0/recommendations/raptor_integration/raptor_recommendation_blocks/#other-customers-have-also-seen-block) - **Recommendations: Commerce** - shows recommendations based on visitors' purchase history (buy and basket events): - [Other customers have also purchased block](https://doc.ibexa.co/projects/userguide/en/6.0/recommendations/raptor_integration/raptor_recommendation_blocks/#other-customers-have-also-purchased-block) - [The Personal Shopping Assistant](https://doc.ibexa.co/projects/userguide/en/6.0/recommendations/raptor_integration/raptor_recommendation_blocks/#the-personal-shopping-assistant-block) - [The Personal Shopping Assistant (additional sales)](https://doc.ibexa.co/projects/userguide/en/6.0/recommendations/raptor_integration/raptor_recommendation_blocks/#the-personal-shopping-assistant-additional-sales-block) - [The Personal Shopping Assistant (conversion)](https://doc.ibexa.co/projects/userguide/en/6.0/recommendations/raptor_integration/raptor_recommendation_blocks/#the-personal-shopping-assistant-conversion-block) - [User's item history](https://doc.ibexa.co/projects/userguide/en/6.0/recommendations/raptor_integration/raptor_recommendation_blocks/#users-item-history-block) ![Recommendation blocks](https://doc.ibexa.co/en/saas/recommendations/raptor_integration/img/recommendation_blocks.png) For a complete description of Recommendation blocks see [Recommendation blocks in User Documentation](https://doc.ibexa.co/projects/userguide/en/6.0/recommendations/raptor_integration/raptor_recommendation_blocks/). ### Advanced usage for complex tracking scenarios For more complex tracking requirements, [PHP API](https://doc.ibexa.co/en/saas/recommendations/raptor_integration/tracking_php_api/index.md) provides direct access to the service. It lets you track custom user actions, create more detailed tracking logic, and support scenarios not covered by the standard setups. ## Benefits ### Understand user behavior Thanks to tracking functions, you can capture how users interact with your products and content, giving you valuable insights into their behavior. It helps you make data-driven decisions to improve engagement and personalize the user experience. ### Highlight recommendations Use recommendation blocks on your websites to highlight content targeted at your customers. Deliver relevant content and build trust in your brand. ### Suggest content to boost retention Help users find content of their interest quicker. Showing visitors content and products that match their interests helps keep them engaged and encourages them to come back. ### Meet customer expectations and increase engagement Recommendation blocks highlight products that match customers' interests. Tailored content boosts engagement by showing visitors information and products that align with their interests and fulfill their needs. This strengthens the connection between your brand and your audience, encouraging them to spend more time on your site and return more often. ### Increase average order value Use tracking for predictive analysis and find out what motivates users to put extra items into their carts. Start building predictions of their behaviors and suggest products your visitors are willing to buy. ### Track performance and increase conversions Use the Raptor service in your Commerce shop and see how recommendations drive sales. Keep track of which recommendations are shown to visitors and measure conversion rates to evaluate their effectiveness against your goals. ### Flexible tracking with PHP API Tracking using PHP API gives you full control over how events are recorded and processed. You can use it for complex scenarios, so tracking can be adapted to your specific needs or business requirements. # Install and configure Raptor > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). To configure the Raptor integration, follow the step-by-step procedure described below. To configure the [Raptor](https://www.raptorservices.com/) integration add-on, follow the step-by-step procedure below. ## Install Raptor connector Before you can proceed to configuring the integration with Raptor, install the Raptor connector. To do it, run the following command: ```bash composer require ibexa/connector-raptor ``` > **Note: Note** > > The [Ibexa Messenger](https://doc.ibexa.co/en/saas/infrastructure_and_maintenance/background_tasks/index.md) package is installed automatically as a dependency, but must be configured to enable server-side tracking. See the Ibexa Messenger documentation for [configuration](https://doc.ibexa.co/en/saas/infrastructure_and_maintenance/background_tasks/#configure-package) details. ## SiteAccess-aware configuration To configure the Raptor connector, use the `ibexa.system..connector_raptor` configuration key in the `config/packages/ibexa_connector_raptor.yaml` file: ```yaml ibexa: system: : connector_raptor: enabled: true customer_id: "12345" # Required tracking_type: client # One of: "client", "server", or "hybrid" # Raptor Recommendations API key recommendations_api_key: "your_api_key_here" # Required # Raptor Recommendations API URI, optional, set by default recommendations_api_uri: '%ibexa.connector.raptor.recommendations.api_uri%' # Cookie lifetime in days for server-side tracking identifier # Default: 365 days. Minimum: 1 day. cookie_id_lifetime_days: 365 ``` - `enabled` - enables or disables the connector for a given scope. Default value: `true`. If set to `false`, no tracking or recommendation requests are executed. - `customer_id` - an identifier used to authenticate requests to the recommendation engine. You can find this value as ["Account number"](https://doc.ibexa.co/en/saas/recommendations/raptor_integration/connector_installation_configuration/#customer-id) in Raptor Control Panel. - `tracking_type` - defines how user events are sent to the tracking API. Default value: `client`. Possible values: - `client` - tracking is executed in the browser using JavaScript snippets generated by the Twig functions and included in the templates. This approach may be blocked by ad blockers. - `server` - tracking is handled on the backend, with events sent directly to the tracking API. It's not affected by ad blockers. - `hybrid` - tracking is executed in the browser by a first-party JavaScript provided by Cohesivo instead of Raptor and then forwarded by the Cohesivo server to the Raptor SaaS. For more information, see [Hybrid tracking](https://doc.ibexa.co/en/saas/recommendations/raptor_integration/hybrid_tracking/index.md). - `recommendations_api_key` - an API key used to authenticate requests to the Recommendations API. This key allows the connector to retrieve personalized recommendations from the recommendation engine. You can find this value as ["API key"](https://doc.ibexa.co/en/saas/recommendations/raptor_integration/connector_installation_configuration/#recommendations-api-key) in Raptor Control Panel. - `recommendations_api_uri` (optional) - overrides the default Raptor address, do not set it unless a custom endpoint is required. - `cookie_id_lifetime_days` (optional) - the lifetime in days of the server-side tracking identifier cookies. Default value: `365` days. Minimum value: `1` day. By default, `tracking_type` is set to `client` as client-side tracking is the standard Raptor mode. To understand the differences between client and server tracking types, including their advantages and disadvantages, refer to the [Raptor documentation](https://content.raptorservices.com/help-center/client-side-vs.-server-side-tracking). > **Note: Note** > > Only one tracking mode can be enabled at a time. Client-side and server-side tracking cannot be used together. ### Customer ID To find the value for the `customer_id` identifier, log in to Raptor Control Panel, and look for "Account number": A. In the top-left corner, above the account name, you can find the account number for the currently active account. B. Click the arrow icon in the top-left corner to expand the window. There you can see a list of all your accounts, with their numbers shown in the “Account number” column on the right. This way, if you have multiple accounts, you can locate and copy the number of any of your accounts, not just the active one. ![Account number](https://doc.ibexa.co/en/saas/recommendations/raptor_integration/img/account_number.png) ### Recommendations API key To find the value for the `recommendations_api_key`, log in to Raptor Control Panel, and look for "API key". To do it, in the left panel, open the **Recommendations** section, and select **Website**. Next, click on the Web module you’re interested in. In the top-right corner, click the three-dot icon and select **API information**. A new window appears, where you can find the "API key" value. Click **Show API information** and copy the value. ![API key](https://doc.ibexa.co/en/saas/recommendations/raptor_integration/img/api_key.png) ## Global configuration (non-SiteAccess-aware) The following settings are global and apply to the entire application (they are not scoped per SiteAccess): - `strict_exceptions` – when enabled, tracking exceptions are thrown instead of being silently handled. Default value: `%kernel.debug%`. - `hybrid_tracking_proxy_path` - by default, it's set to `/raptor/track`. The client-side shim sends tracking events to this same-origin endpoint, which forwards them to Raptor asynchronously. This value can be overridden in `config/packages/ibexa_connector_raptor.yaml` file, for example: ```yaml ibexa: system: : connector_raptor: enabled: true customer_id: "12345" # Required tracking_type: client # One of: "client", "server", or "hybrid" # Raptor Recommendations API key recommendations_api_key: "your_api_key_here" # Required # Raptor Recommendations API URI, optional, set by default recommendations_api_uri: '%ibexa.connector.raptor.recommendations.api_uri%' # Cookie lifetime in days for server-side tracking identifier # Default: 365 days. Minimum: 1 day. cookie_id_lifetime_days: 365 ibexa_connector_raptor: # When enabled, tracking exceptions are thrown instead of being silently handled strict_exceptions: true hybrid_tracking_proxy_path: '/raptor/track' ``` # Tracking functions > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). Integrate the tracking script to collect user interactions. [Raptor connector](https://doc.ibexa.co/en/saas/recommendations/raptor_integration/raptor_connector/index.md) introduces [visit tracking functionality](https://content.raptorservices.com/help-center/introduction-to-tracking-documentation) for collecting user interactions with products and content. The implementation includes product visit tracking with mapping to tracking parameters, and Twig functions for straightforward integration. Raptor integration introduces two Twig functions: - `ibexa_tracking_script()` - allows you to embed main tracking script into the website. - `ibexa_tracking_track_event()` - is responsible for sending event data to the service, enabling tracking of user interactions and behaviors. ## Embed tracking script You must embed the tracking script into the website’s layout to enable tracking. To do it, add the `ibexa_tracking_script()` Twig function into the section of your base layout template, for example, `@ibexadesign/pagelayout.html.twig`: ```html+twig {# templates/pagelayout.html.twig #} {# ... other head content ... #} {# Initialize Raptor tracking - must be called before any tracking events #} {{ ibexa_tracking_script() }} {# ... page content ... #} ``` ## Tracking modes Tracking user interactions can be implemented on the client-side or the server-side. Each approach differs in where events are captured and how they are sent to the tracking backend. The [tracking Twig function](#embed-tracking-script) outputs different content depending on the mode: ```yaml # Server-side tracking connector_raptor: tracking_type: 'server' # Returns nothing (prod) or HTML comments (dev) # Client-side tracking connector_raptor: tracking_type: 'client' # Returns ``` ### Example custom integration Example custom integration with [TermsFeed](https://www.termsfeed.com/): ````html ``` html ```` ``` ``` # Hybrid tracking > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). Enable hybrid tracking to avoid ad blockers and proxy events through your server. Hybrid tracking mode is an additional tracking mode available alongside [`client` and `server`](https://doc.ibexa.co/en/saas/recommendations/raptor_integration/tracking_functions/index.md). In hybrid mode, the bundle includes a client-side [shim]() that captures Raptor tracking events and sends them to a same-origin endpoint instead of communicating directly with Raptor servers. The server enriches each event with identifiers resolved from request cookies (`cookieId`, `sessionId`, and `userId`) and forwards it to Raptor asynchronously through [Ibexa Messenger](https://doc.ibexa.co/en/saas/infrastructure_and_maintenance/background_tasks/index.md). Since the browser never connects to the Raptor domain, ad blockers cannot block the requests. ## Hybrid vs server or client-side tracking Both `server` and `hybrid` tracking modes deliver pageviews and events server-side, so tracking requests are not affected by ad blockers. The main difference is that `hybrid` mode loads a first-party tracking JavaScript (`raptor-proxy.js`) provided by the Cohesivo instance, instead of the Raptor SaaS JavaScript. The Raptor script itself (`//deliver.raptorstatic.com/script/raptor-3.0.min.js`) is loaded only in `client` mode. The browser script only forwards captured tracking events to the same-origin proxy endpoint on your Cohesivo instance. First-party visitor cookies are created and refreshed server-side, which is what helps them survive Safari [Intelligent Tracking Prevention](https://webkit.org/blog/7675/intelligent-tracking-prevention/). Event tracking still happens on the server, so ad blockers cannot block it. The main advantage of `hybrid` mode over `server` mode is its ability to capture client-side tracking events. Visitor cookies are also refreshed more frequently than in `server` mode. Check the table below to compare the behavior of different tracking modes: | Tracking type | Browser script | Event delivery | Works with ad blockers | Cookie refresh | | ------------- | -------------- | -------------- | ---------------------- | ------------------------------------- | | client | yes | Browser | no | yes | | server | no | Server | yes | yes (server-side, on full page loads) | | hybrid | yes | Server | yes | yes | ## Hybrid tracking flow When hybrid tracking is enabled, `TrackingScriptExtension` renders the proxy bootstrap template and loads the `raptor-proxy.js` shim, which replaces the `window.raptor.push` function and sends out the events queued before the tracking consent was given. The shim sends each captured event to the proxy endpoint as a separate POST request. The `TrackingProxyController::track` action validates the `EventPayloadParser` payload, enriches each event with identifiers resolved from cookies, and dispatches one `TrackProxiedEventMessage` for every event. Messages are then processed asynchronously via Ibexa Messenger and `TrackProxiedEventMessageHandler` ultimately forwards them to Raptor through `TrackingService::trackRawParameters`. ### Hybrid tracking configuration To configure the Raptor hybrid tracking, use the `ibexa.system..connector_raptor` configuration key in the `config/packages/ibexa_connector_raptor.yaml` file: ```yaml ibexa_connector_raptor: hybrid_tracking_proxy_path: '/raptor/track' ibexa: system: default: connector_raptor: enabled: true customer_id: '' tracking_type: hybrid ``` > **Note: Note** > > The `hybrid_tracking_proxy_path` setting is configured globally, while `enabled`, `customer_id`, and `tracking_type` are configured per SiteAccess. ### Routing import Hybrid tracking requires routing import. It means that the proxy endpoint route must be imported in the project, for example in the `config/routes/ibexa_connector_raptor.yaml` file: ```yaml ibexa.connector_raptor: resource: '@IbexaConnectorRaptorBundle/Resources/config/routing.yaml' ``` This import is required only in `hybrid` mode. Without it, the proxy endpoint returns 404 responses and tracking events are not processed. ### Proxy path configuration You can configure the proxy endpoint path globally. By default, it's set to `/raptor/track`. The client-side shim sends tracking events to this same-origin endpoint, which forwards them to Raptor asynchronously. ```yaml ibexa_connector_raptor: hybrid_tracking_proxy_path: '/track' ``` > **Note: Note** > > The path must start with a `/` symbol and cannot overlap with any existing routes. The route is generated from this value, so updating it automatically updates the route as well. You don't need to modify the routing import. After changing the configuration, clear the application cache: ```bash php bin/console cache:clear ``` Verify that the route has been registered correctly. To do it, run the following command: ```bash php bin/console debug:router | grep raptor ``` ### Template usage By default, the tracking script waits for user consent before sending tracking events. The Twig API remains identical regardless of the configured tracking mode: ```html+twig {{ ibexa_tracking_script() }} {{ ibexa_tracking_track_event('visit', product) }} ``` # Tracking with PHP API > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). Tracking with PHP API. You can interact directly with the [Raptor connector](https://doc.ibexa.co/en/saas/recommendations/raptor_integration/raptor_connector/index.md)'s service using the [PHP API](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/namespaces/ibexa-contracts-connectorraptor-tracking.html) for advanced tracking usage. ## Advanced usage – direct interaction with the service The [`ServerSideTrackingDispatcherInterface::dispatch()`](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-ConnectorRaptor-Tracking-ServerSideTrackingDispatcherInterface.html#method_dispatch) method allows sending tracking data from the server side. It can be used in controllers, event subscribers, or any other part of the application. This method receives an [`EventDataInterface`](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-ConnectorRaptor-Tracking-Event-EventDataInterface.html). For more information, see the available events in the [tracking event namespace](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/namespaces/ibexa-contracts-connectorraptor-tracking-event.html). ### Mapping event data The recommended method is [`EventMapperInterface::map()`](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-ConnectorRaptor-Tracking-EventMapperInterface.html#method_map). This method receives an [`EventType`](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-ConnectorRaptor-Tracking-EventType.html#cases) case, a data depending on the event type (a [`ProductInterface`](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-ProductCatalog-Values-ProductInterface.html), a [`Content`](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-Core-Repository-Values-Content-Content.html), or a `string`), and a context's associative array that uses [`EventContext`](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-ConnectorRaptor-Tracking-EventContext.html) constants as keys. For more information, see the same arguments of the `ibexa_tracking_track_event` Twig function. | Event type | Data class | Context keys | | -------------------------- | ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `EventType::VISIT` | `ProductInterface` | (optional) `EventContext::CATEGORY_IDENTIFIER`, (optional) `EventContext::WEBSITE_ID` | | `EventType::CONTENT_VISIT` | `Content` | (optional) `EventContext::WEBSITE_ID` | | `EventType::BUY` | `ProductInterface` | `EventContext::SUBTOTAL`, `EventContext::CURRENCY`, `EventContext::QUANTITY`, (optional) `EventContext::CATEGORY_IDENTIFIER`, (optional) `EventContext::WEBSITE_ID` | | `EventType::BASKET` | `ProductInterface` | `EventContext::BASKET_CONTENT`, `EventContext::BASKET_ID`, (optional) `EventContext::CATEGORY_IDENTIFIER`, (optional) `EventContext::QUANTITY`, (optional) `EventContext::WEBSITE_ID` | | `EventType::ITEM_CLICK` | `string` (product code) | `EventContext::MODULE_NAME`, `EventContext::REDIRECT_URL` | | `EventType::PAGEVIEW` | `string` (URL) | `EventContext::URL` (required), (optional) `EventContext::WEBSITE_ID` | Check the following example: ```php use Ibexa\Contracts\ConnectorRaptor\Tracking\EventContext; use Ibexa\Contracts\ConnectorRaptor\Tracking\EventMapperInterface; use Ibexa\Contracts\ConnectorRaptor\Tracking\EventType; use Ibexa\Contracts\ConnectorRaptor\Tracking\ServerSideTrackingDispatcherInterface; //… // Map product to VisitEventData automatically, override its category $eventData = $this->eventMapper->map(EventType::VISIT, $product, [ EventContext::CATEGORY_IDENTIFIER => 'electronics', ]); // Send tracking event $this->trackingDispatcher->dispatch($eventData); ``` ### Category parameter for product events In Cohesivo, products can be assigned to multiple categories. However, Raptor accepts only a single category value in tracking events. By default, the connector uses the first category from the list of categories assigned to the product. You can override this behavior and define which category is sent in tracking events. To do this: 1. Open the product page in the back office. 2. Check the categories assigned to the product and select the one you want to use. 3. Copy the identifier. 4. Pass this identifier as the category parameter in the tracking event. > **Note: Note** > > This option applies only to product-related tracking events. Example: ```text {% block content %}

    {{ product.name }}

    {# ... product content ... #}
    {# Track with category identifier - automatic loading and formatting #} {{ ibexa_tracking_track_event('visit', product, { 'categoryIdentifier': 'electronics' }) }} {% endblock %} ``` ### Manual `EventData` creation Manual creation of `EventData` allows precise control over the events sent to the service. It enables you to define custom event parameters, track specific user interactions, and tailor data collection to advanced use cases. Check the following example: ```php use Ibexa\Contracts\ConnectorRaptor\Tracking\Event\VisitEventData; use Ibexa\Contracts\ConnectorRaptor\Tracking\ServerSideTrackingDispatcherInterface; // … $eventData = new VisitEventData( productCode: $product->getCode(), productName: $product->getName(), categoryPath: '25#Electronics;26#Smartphones', // Build manually currency: 'USD', itemPrice: '999.99' ); $this->trackingDispatcher->dispatch($eventData); ``` `categoryPath` parameter sets the category path for recommendations and needs to be composed manually following the specified format and rules: - format: `CategoryId#CategoryName;CategoryId#CategoryName`, for example, `25#Electronics;26#Smartphones` - if `CategoryName` is missing, repeat the ID, for example, `25#25;26#26` - if `CategoryId` is missing, use the `CategoryName`, for example, `Electronics;Smartphones` For more information, see the available events in the [tracking event namespace](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/namespaces/ibexa-contracts-connectorraptor-tracking-event.html). ### Example - event subscriber If you need to track [events](https://doc.ibexa.co/en/saas/api/event_reference/event_reference/index.md) automatically based on application events, you can use an event subscriber. It reacts to specific events in the application and triggers tracking logic without the need to add it manually in templates. ```php ['onResponse', -10]]; } public function onResponse(ResponseEvent $event): void { if (!$event->isMainRequest()) { return; } $request = $event->getRequest(); // Example: track only if request has specific attribute $product = $request->attributes->get('product'); if (null === $product) { return; } $eventData = $this->eventMapper->map(EventType::VISIT, $product); $this->trackingDispatcher->dispatch($eventData); } } ``` You can also use Cohesivo events. For more information, see [Event reference](https://doc.ibexa.co/en/saas/api/event_reference/event_reference/index.md). # Recommendation blocks in Page Builder > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). Recommendation blocks in Page Builder Editions: Experience One of the Raptor Integration elements is the introduction of recommendation blocks available in the [Page Builder](https://doc.ibexa.co/en/saas/content_management/pages/page_builder_guide/index.md). Content, Product, and Commerce recommendations can be added to a landing page using the blocks. Editors can configure these blocks to display: - personalized product recommendations - related articles or content - recently viewed or popular items In the toolbar, corresponding categories for recommendation blocks are available, containing sets of blocks depending on the recommendation type: - **Recommendations: Content** - presents content recommendations: - [Content that has been seen along with the item category](https://doc.ibexa.co/projects/userguide/en/6.0/recommendations/raptor_integration/raptor_recommendation_blocks/#content-that-has-been-seen-along-with-the-item-category-block) - [Merchandising content sorted by personal preferences and popularity](https://doc.ibexa.co/projects/userguide/en/6.0/recommendations/raptor_integration/raptor_recommendation_blocks/#merchandising-content-sorted-by-personal-preferences-and-popularity) - [Most popular content](https://doc.ibexa.co/projects/userguide/en/6.0/recommendations/raptor_integration/raptor_recommendation_blocks/#most-popular-content-block) - [Other customers have also seen this content](https://doc.ibexa.co/projects/userguide/en/6.0/recommendations/raptor_integration/raptor_recommendation_blocks/#other-customers-have-also-seen-this-content-block) - [Personalized content recommendations](https://doc.ibexa.co/projects/userguide/en/6.0/recommendations/raptor_integration/raptor_recommendation_blocks/#personalized-content-recommendations-block) - [User’s content history](https://doc.ibexa.co/projects/userguide/en/6.0/recommendations/raptor_integration/raptor_recommendation_blocks/#users-content-history-block) - **Recommendations: Product** - displays product suggestions based on visitors’ browsing history: - [Items associated with the given Content](https://doc.ibexa.co/projects/userguide/en/6.0/recommendations/raptor_integration/raptor_recommendation_blocks/#items-associated-with-the-given-content-block) - [Items of Customized Feeds sorted by personal preferences and popularity or trendiness](https://doc.ibexa.co/projects/userguide/en/6.0/recommendations/raptor_integration/raptor_recommendation_blocks/#items-of-customized-feeds-sorted-by-personal-preferences-and-popularity-or-trendiness) - [Most popular products](https://doc.ibexa.co/projects/userguide/en/6.0/recommendations/raptor_integration/raptor_recommendation_blocks/#most-popular-products-block) - [Most popular products in category](https://doc.ibexa.co/projects/userguide/en/6.0/recommendations/raptor_integration/raptor_recommendation_blocks/#most-popular-products-in-category-block) - [Other customers have also seen](https://doc.ibexa.co/projects/userguide/en/6.0/recommendations/raptor_integration/raptor_recommendation_blocks/#other-customers-have-also-seen-block) - **Recommendations: Commerce** - shows recommendations based on visitors' purchase history (buy and basket events): - [Other customers have also purchased block](https://doc.ibexa.co/projects/userguide/en/6.0/recommendations/raptor_integration/raptor_recommendation_blocks/#other-customers-have-also-purchased-block) - [The Personal Shopping Assistant](https://doc.ibexa.co/projects/userguide/en/6.0/recommendations/raptor_integration/raptor_recommendation_blocks/#the-personal-shopping-assistant-block) - [The Personal Shopping Assistant (additional sales)](https://doc.ibexa.co/projects/userguide/en/6.0/recommendations/raptor_integration/raptor_recommendation_blocks/#the-personal-shopping-assistant-additional-sales-block) - [The Personal Shopping Assistant (conversion)](https://doc.ibexa.co/projects/userguide/en/6.0/recommendations/raptor_integration/raptor_recommendation_blocks/#the-personal-shopping-assistant-conversion-block) - [User's item history](https://doc.ibexa.co/projects/userguide/en/6.0/recommendations/raptor_integration/raptor_recommendation_blocks/#users-item-history-block) ![Recommendation blocks](https://doc.ibexa.co/en/saas/recommendations/raptor_integration/img/recommendation_blocks.png) After opening the settings of a recommendation block, a link is available at the bottom of the window. It leads to the [Raptor Control Panel](https://controlpanel.raptorsmartadvisor.com/) (opens in a separate tab), where you can configure advanced settings and fine-tune the recommendation strategy. ![Advanced settings](https://doc.ibexa.co/en/saas/recommendations/raptor_integration/img/advanced_settings.png) For a complete description of Recommendation blocks see [Recommendation blocks](https://doc.ibexa.co/projects/userguide/en/6.0/recommendations/raptor_integration/raptor_recommendation_blocks/). For the list of all page blocks that are available in Page Builder, see [Block reference page](https://doc.ibexa.co/projects/userguide/en/6.0/content_management/block_reference/). # Render recommendations > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). Use existing controllers to render recommendations outside the Page Builder. You can use existing controllers to render [recommendations](https://doc.ibexa.co/en/saas/recommendations/raptor_integration/recommendation_blocks/index.md) outside the Page Builder. The controllers responsible for rendering block recommendations on the front-end are independent and can be used to render recommendations for specific strategies. Each controller can be used to retrieve and display recommendations within a Twig template as follows: ```html+twig {{ render(controller('', { 'limit': limit, 'template': '@ibexadesign/.html.twig', '': parameter_value })) }} ``` The controllers are placed in the `Ibexa\Bundle\ConnectorRaptor\Controller\Block` namespace. Each controller always requires these two parameters: - **limit** – the number of recommendations to render - **template** – the path to the template Any other required parameters are specific to each controller and are detailed in the **Parameters** column of the table below: | Block name | Controller | Parameters | Recommendation item type | | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------- | --------------------------------------------------------------------------------------- | ------------------------ | | [Content that have been seen along with the item category](https://doc.ibexa.co/projects/userguide/en/6.0/recommendations/raptor_integration/raptor_recommendation_blocks/#content-that-has-been-seen-along-with-the-item-category-block) | `ContentBasedOnProductCategoryBlockController` `::showAction()` | `categoryId` (integer), `limit` (integer), `template` (string) | Content | | [Items associated with the given Content](https://doc.ibexa.co/projects/userguide/en/6.0/recommendations/raptor_integration/raptor_recommendation_blocks/#items-associated-with-the-given-content-block) | `ItemsBasedOnContentBlockController` `::showAction()` | `contentId` (integer), `limit` (integer), `template` (string) | Product | | [Items of Customized Feeds sorted by personal preferences and popularity or trendiness](https://doc.ibexa.co/projects/userguide/en/6.0/recommendations/raptor_integration/raptor_recommendation_blocks/#items-of-customized-feeds-sorted-by-personal-preferences-and-popularity-or-trendiness) | `MerchandisingItemsBlockController` `::showAction()` | `merchandisingCampaignId` (string), `limit` (integer), `template` (string) | Product | | [Merchandising content sorted by personal preferences and popularity](https://doc.ibexa.co/projects/userguide/en/6.0/recommendations/raptor_integration/raptor_recommendation_blocks/#merchandising-content-sorted-by-personal-preferences-and-popularity) | `MerchandisingContentBlockController` `::showAction()` | `merchandisingCampaignId` (string), `limit` (integer), `template` (string) | Content | | [Most popular content](https://doc.ibexa.co/projects/userguide/en/6.0/recommendations/raptor_integration/raptor_recommendation_blocks/#most-popular-content-block) | `PopularContentBlockController` `::showAction()` | `limit` (integer), `template` (string) | Content | | [Most popular products](https://doc.ibexa.co/projects/userguide/en/6.0/recommendations/raptor_integration/raptor_recommendation_blocks/#most-popular-products-block) | `PopularItemsBlockController` `::showAction()` | `showInStock` (boolean), `limit` (integer), `template` (string) | Product | | [Most popular products in category](https://doc.ibexa.co/projects/userguide/en/6.0/recommendations/raptor_integration/raptor_recommendation_blocks/#most-popular-products-in-category-block) | `PopularItemsInCategoryBlockController` `::showAction()` | `categoryId` (integer), `showInStock` (boolean), `limit` (integer), `template` (string) | Product | | [Other customers have also seen](https://doc.ibexa.co/projects/userguide/en/6.0/recommendations/raptor_integration/raptor_recommendation_blocks/#other-customers-have-also-seen-block) | `SimilarItemsBlockController` `::showAction()` | `productCode` (string), `showInStock` (boolean), `limit` (integer), `template` (string) | Product | | [Other customers have also seen this content](https://doc.ibexa.co/projects/userguide/en/6.0/recommendations/raptor_integration/raptor_recommendation_blocks/#other-customers-have-also-seen-this-content-block) | `SimilarContentBlockController` `::showAction()` | `contentId` (integer), `limit` (integer), `template` (string) | Content | | [Other Customers Have also Purchased block](https://doc.ibexa.co/projects/userguide/en/6.0/recommendations/raptor_integration/raptor_recommendation_blocks/#other-customers-have-also-purchased-block) | `OtherCustomersAlsoPurchasedBlockController` `::showAction()` | `productCode` (string), `limit` (integer), `template` (string) | Product | | [Personalized content recommendations](https://doc.ibexa.co/projects/userguide/en/6.0/recommendations/raptor_integration/raptor_recommendation_blocks/#personalized-content-recommendations-block) | `UserContentRecommendationsBlockController` `::showAction()` | `limit` (integer), `template` (string) | Content | | [The Personal Shopping Assistant](https://doc.ibexa.co/projects/userguide/en/6.0/recommendations/raptor_integration/raptor_recommendation_blocks/#the-personal-shopping-assistant-block) | `UserItemRecommendationsBlockController` `::showAction()` | `productCode` (string), `showInStock` (boolean), `limit` (integer), `template` (string) | Product | | [The Personal Shopping Assistant (additional sales)](https://doc.ibexa.co/projects/userguide/en/6.0/recommendations/raptor_integration/raptor_recommendation_blocks/#the-personal-shopping-assistant-additional-sales-block) | `UserCrossSellingBlockController` `::showAction()` | `showInStock` (boolean), `limit` (integer), `template` (string) | Product | | [The Personal Shopping Assistant (conversion)](https://doc.ibexa.co/projects/userguide/en/6.0/recommendations/raptor_integration/raptor_recommendation_blocks/#the-personal-shopping-assistant-conversion-block) | `UserCrossSellingBlockController` `::showAction()` | `showInStock` (boolean), `limit` (integer), `template` (string) | Product | | [User's item history](https://doc.ibexa.co/projects/userguide/en/6.0/recommendations/raptor_integration/raptor_recommendation_blocks/#users-item-history-block) | `UserItemHistoryBlockController` `::showAction()` | `showInStock` (boolean), `limit` (integer), `template` (string) | Product | Each template receives a `recommendations` Twig variable, which is a list with either [`Ibexa\Contracts\ProductCatalog\Values\ProductInterface`](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-ProductCatalog-Values-ProductInterface.html) instances for product recommendations or [`Ibexa\Contracts\Core\Repository\Values\Content\Content`](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-Core-Repository-Values-Content-Content.html) instances for content recommendations. Two generic templates are provided and can be used in `./templates/themes/` directory: - `@ibexadesign/ibexa/recommendations/_content_list.html.twig` for content items: ```html+twig {% if recommendations is not empty %} {% for content in recommendations %} {% set location = content.contentInfo.mainLocation %} {% if location %}

    {{ ibexa_content_name(content) }}

    {% else %}

    {{ ibexa_content_name(content) }}

    {% endif %} {% endfor %} {% endif %} ``` - `@ibexadesign/ibexa/recommendations/_product_list.html.twig` for products: ```html+twig {% if recommendations is not empty %} {% endif %} ``` To fetch recommendations for the remaining modules, you need to [create a custom controller](https://doc.ibexa.co/en/saas/templating/queries_and_controllers/controllers/index.md) and use a method from [`Ibexa\Contracts\ConnectorRaptor\Recommendations\RecommendationsServiceInterface`](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-ConnectorRaptor-Recommendations-RecommendationsServiceInterface.html). Use this method to display recommendations on any page, for example, on a specific product page, as shown below: ![Custom rendering](https://doc.ibexa.co/en/saas/recommendations/raptor_integration/img/custom_rendering.png) # Customer Data Platform # Raptor CDP integration > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). Raptor CDP is a software system designed to collect and organize customer data from multiple sources to build comprehensive customer profiles. Editions: Experience ## What is Raptor CDP Raptor CDP (Customer Data Platform) helps you solve one of the hardest challenges facing business world today: building unique experiences for your customers. With Raptor CDP you're able to track and aggregate data of your customers' activity on multiple channels. It allows you to create individual customer profiles that enable you to personalize their experience on your platform. ![Raptor CDP control panel](https://doc.ibexa.co/en/saas/raptor_cdp/img/raptor_cdp_control_panel.png) ## How it works Raptor CDP unifies customer data across your organization to help you activate your users and provide them with real-time engagement. With defined audiences you can target your user segments at the right time, through the most used channel, with the relevant message, content, or products. The customer data are collected through the system of trackers embedded in different parts of your page. For more information on activation and trackers, see [CDP activation documentation](https://doc.ibexa.co/en/saas/raptor_cdp/raptor_cdp_activation/raptor_cdp_activation/index.md). # Raptor CDP product guide > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). The Raptor CDP product guide describes all the possibilities that the Customer Data Platform offers to help you build great customer experiences. Editions: Experience ## What is Raptor CDP Raptor CDP (Customer Data Platform) module helps you build unique and memorable experiences for your customers. By using Raptor CDP you can monitor and compile data about your customers' activity on multiple channels. It also allows you to create individual customer profiles so you can customize their experience on your platform. With Raptor CDP you can store and manage large volumes of customer data in a structured manner. This central data storage supports business growth with a scalable infrastructure, helping to futureproof your business. You can get customer data from both online and offline data sources. It includes first, second, and third-party data from multiple sources such as transactional systems, website tracking, and behavior, POS, CRM, and others. ## Availability Raptor CDP is available in Ibexa Experience and Ibexa Commerce editions. ## How does Raptor CDP work Raptor CDP unifies customer data throughout your whole organization. It helps you activate your users and give them real-time interaction. You can target certain user segments with the appropriate message, content, or products at the right time through the most used channels by using specified audiences. Customer data is gathered through a system of trackers embedded in various areas of your website. ![CDP - how does it work](https://doc.ibexa.co/en/saas/raptor_cdp/img/cdp.png) ### Installation and configuration To start using Raptor CDP, first you need to contact your sales representative, who provides you with a link to [register your Raptor CDP account](https://doc.ibexa.co/en/saas/raptor_cdp/raptor_cdp_installation/#register-in-raptor-cdp-dashboard). When you're done with registration process, you're able to access a separate instance with the data needed to configure, activate, and use this feature. After your account is created, you can [download and install the Raptor CDP package](https://doc.ibexa.co/en/saas/raptor_cdp/raptor_cdp_installation/#install-package) that is opt-in and needs to be downloaded separately. Last step is to go through the [configuration process](https://doc.ibexa.co/en/saas/raptor_cdp/raptor_cdp_activation/raptor_cdp_configuration/index.md). ### Customer profile In Raptor CDP you can build 360° customer profiles. It unifies customer data from different sources to help you understand your prospects and customer needs. After you get customer data, you can unify and match customer profiles based on their preferences and habits. You can create and analyze complete, 360° customer profiles based on demographics, interactions, behaviors, and transactional data. This approach helps you create a single customer view. ![Customer profile](https://doc.ibexa.co/en/saas/raptor_cdp/img/customer_profile.png) ### Segment groups To create a personalized customer experience, you need to group your clients into specified audiences. Cohesivo comes with a ready solution - segment groups. Segment group information is reused by various Cohesivo functionalities, such as [Recommendations](https://doc.ibexa.co/en/saas/recommendations/raptor_integration/raptor_connector_guide/index.md) or content targeting. You can [create a segment group](https://doc.ibexa.co/en/saas/administration/admin_panel/segments_admin_panel/index.md) in the back office of Cohesivo. It serves as a container for all segments data generated by Raptor CDP. When you create a segment group, you need to provide its name and identifier. Be careful while doing so, as after you create the segment group in the back office and connect it to Raptor CDP, you cannot change it in any way, including edit its name. Remember to add a segment group identifier to the configuration, under the `segment_group_identifier` field. ## Capabilities ### Data export Configuration in Raptor CDP allows you to automate the process of exporting content, users, and products. An `ibexa_cdp.data_export` [configuration key](https://doc.ibexa.co/en/saas/raptor_cdp/raptor_cdp_data_export_schedule/#configuration-key) includes the `schedule` setting where you can find separate sections for exporting user, content, and product. Structure of each section is exactly the same and includes `interval` and `options` elements: - `interval` - sets the frequency at which the command is invoked, uses cron expressions, for example, '\*/30 * * * \*' means "every 30 minutes", '0 \*/12 * * \*' means "every 12th hour" - `options` - allows you to add arguments that have to be passed to the export command This configuration allows you to provide multiple export workflows with parameters. It's important, because all the types of content/product must have their own parameters on the CDP side, where each has a different Stream ID key and different required values configured per data source. Regarding data export, currently, only Stream File transport is supported and can be initialized from the configuration. For more information, see [CDP data export](https://doc.ibexa.co/en/saas/raptor_cdp/raptor_cdp_activation/raptor_cdp_data_export/index.md). ### Data customization ​You can customize content and product data exported to Raptor CDP and control what field type information you want to export. With Raptor CDP, you can export field types and field type values. They're exported with metadata and attributes, for example, ID, field definition name, type, or value. For more information, see [data customization](https://doc.ibexa.co/en/saas/raptor_cdp/raptor_cdp_data_customization/index.md) documentation in Developer Documentation. ### Client-side Tracking The final step is setting up a tracking script. For more information, see [CDP add client-side tracking](https://doc.ibexa.co/en/saas/raptor_cdp/raptor_cdp_activation/raptor_cdp_add_tracking/index.md) and [Introduction to tracking in Raptor documentation](https://content.raptorservices.com/help-center/introduction-to-tracking-documentation). ### Audience Builder In the Audience Builder, you can create audiences - groups of users that meet the assumed conditions. You can choose specific conditions: `did`, `did not`, or `have`. The conditions `did` and `did not` allow you to use events like buy, visit or add to a cart from online tracking. The `have` conditions are tied to personal characteristics and can be used to track the sum of all buys or top-visited categories. You can also connect created audiences to the activations. ![Audience Builder](https://doc.ibexa.co/en/saas/raptor_cdp/img/audience_builder.png) ### Anonymous user segmentation Raptor CDP can build audiences for anonymous users, enabling personalised experiences for not logged-in visitors. When an anonymous visitor accesses your site, Raptor starts building an [anonymous profile](https://content.raptorservices.com/help-center/introduction-to-person-identifiers-and-profile-unification). You can segment these anonymous profiles into different audiences, exactly as in case of logged-in users, and use this information in Cohesivo to provide personalized experiences. ## Benefits ### Personalized user experience With Raptor CDP you can build unique and memorable experience for your customers and create individual customer profiles. By using 360° client profiles, you can connect with the right customer at the right moment, in the right place. Build extensive customer profiles that include their interactions, habits, and preferences from several touchpoints. ### Segment groups Provide a personalized customer experience, group your clients into specified audiences, and provide recommendations depending on the user data. Create segment groups to deliver personalized campaigns to boost engagement and conversion rates. ### Audience Builder Create user groups - audiences - based on conditions and events. ### Data export Export data regarding content, users, and products. Data export includes automatic file mapping. Analyze customer data, track campaigns, and discover the most effective strategies to boost performance. ### Data customization Customize data to control what field type information you want to export. ### Real-time action Deliver relevant interactions in the right place at the right time for optimal results thanks to dynamic, real-time data updates. Take advantage of event-triggered communications which are aligned with your customers immediate interests. # Install Raptor CDP > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). Installation of standalone Raptor CDP package. Editions: Experience There are three steps required to install Raptor CDP. First, you need to register your Raptor CDP account, then you can download a CDP package and update the configuration. ## Register in Raptor CDP dashboard If you decide to acquire Raptor CDP, contact your sales representative to receive a registration link to Raptor CDP. After registration, you get access to a separate instance where you can find data required for configuring, activating, and using this feature. ## Install package Raptor CDP comes in an additional package that is opt-in and needs to be downloaded separately. To download it run: ```bash composer require ibexa/cdp ``` Symfony Flex installs and activates the package. After an installation process is finished, go to `config/packages/security.yaml` and uncomment `ibexa_cdp` rule. ```yaml security: firewalls: # ... ibexa_cdp: request_matcher: Ibexa\Cdp\Security\RequestMatcher custom_authenticators: - 'Ibexa\Cdp\Security\CdpRequestAuthenticator' stateless: true ``` Now, you can configure Raptor CDP. Go to [the activation documentation](https://doc.ibexa.co/en/saas/raptor_cdp/raptor_cdp_activation/raptor_cdp_activation/index.md) and follow the steps. # Activate Raptor CDP > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). Step-by-step activation procedure of Raptor CDP. Editions: Experience Follow a step-by-step procedure that allows you to activate Raptor CDP. Activation includes configuration, data export and adding tracking. - [Configure Raptor CDP](https://ez-systems-developer-documentation--3381.com.readthedocs.build/en/3381/raptor_cdp/raptor_cdp_activation/raptor_cdp_configuration/): Step-by-step configuration procedure of Raptor CDP. - [Export Raptor CDP data](https://ez-systems-developer-documentation--3381.com.readthedocs.build/en/3381/raptor_cdp/raptor_cdp_activation/raptor_cdp_data_export/): Step-by-step data export procedure in Raptor CDP. - [Track with Raptor CDP](https://ez-systems-developer-documentation--3381.com.readthedocs.build/en/3381/raptor_cdp/raptor_cdp_activation/raptor_cdp_add_tracking/): Adding tracking in Raptor CDP. # Configure Raptor CDP > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). Step-by-step configuration procedure of Raptor CDP. Editions: Experience To configure Raptor CDP, use the `ibexa.system..cdp` [configuration key](https://doc.ibexa.co/en/saas/administration/configuration/configuration/#configuration-files): ```yaml ibexa: system: default: cdp: account_number: 123456 data_export: user_data: transport: stream_file stream_file: stream_id: 00000000-00000000-00000000-00000000 content_data: transport: stream_file stream_file: stream_id: 00000000-00000000-00000000-00000000 product_data: transport: stream_file stream_file: stream_id: 00000000-00000000-00000000-00000000 activations: - client_id: '%env(CDP_ACTIVATION_CLIENT_ID)%' client_secret: '%env(CDP_ACTIVATION_CLIENT_SECRET)%' segment_group_identifier: example_segment_group_identifier membership: # For anonymous user segmentation activation_id: '%env(CDP_MEMBERSHIP_ACTIVATION_ID)%' api_key: '%env(CDP_MEMBERSHIP_API_KEY)%' base_url: 'https://cdp-api.raptorsmartadvisor.com' timeout: 5 ``` - `account_number` - a [number](#account-number) obtained from Accounts settings in Raptor CDP dashboard - `stream_id` - stream ID generated when importing data from the stream file in Data Manage - `activations` - activation details. You can configure multiple activations. They have to be of type `Ibexa` in Cohesivo dashboard - `client_id` and `client_secret` - client credentials are used to authenticate against the Webhook endpoint. Make sure they're random and secure - `segment_group_identifier` - a [location](#segment-group) to which CDP data is imported - `membership` - configuration that enables support for [anonymous user segmentation](#anonymous-user-segmentation) - `membership.activation_id` and `membership.api_key` - credentials for the CDP Membership API, required for [anonymous user segmentation](#anonymous-user-segmentation) - `membership.base_url` - base URL of the CDP Membership API (default: `https://cdp-api.raptorsmartadvisor.com`) - `membership.timeout` - timeout in seconds for Membership API requests (default: `5`) ## Account number Now, fill in the account number. Log in to Raptor CDP and in the top right corner, select available accounts. ![List of available accounts](https://doc.ibexa.co/en/saas/raptor_cdp/img/raptor_cdp_accounts.png) A pop-up window displays a list of all available accounts and their numbers. ![Account number](https://doc.ibexa.co/en/saas/raptor_cdp/img/raptor_cdp_account_number.png) ## Segment group Create a segment group in the back office. It serves as a container for all segments data generated by Raptor CDP. Go to **Admin** -> **Segments** and select **Create**. Fill in name and identifier for a segment group. Choose wisely, as once connected to CDP segment group cannot be changed. > **Caution: Raptor CDP segment group** > > After you create the segment group in the back office and connect it to Raptor CDP, you cannot change it in any way, including edit its name. ![Creating a new segment group](https://doc.ibexa.co/en/saas/raptor_cdp/img/raptor_cdp_create_segment_group.png) Next, add a segment group identifier to the configuration. ## Anonymous user segmentation To set up [segmentation for anonymous users](https://doc.ibexa.co/en/saas/raptor_cdp/raptor_cdp_guide/#anonymous-user-segmentation), take the following steps: ### Set up CDP API activation Create an activation of type "CDP API" in the Raptor dashboard. For instructions, see [CDP API activations](https://content.raptorservices.com/help-center/cdp/activations/cdp-api) in Raptor documentation. ### Configure website tracking dataflow Set up a "Website tracking" dataflow with `coid` (cookie ID) as the person identifier so that Raptor can use the tracking data in the CDP. For more information, see [Website tracking dataflow](https://content.raptorservices.com/help-center/tools/datamanager/introduction-to-the-data-manager) in Raptor documentation. ### Configuration Add the `membership.activation_id` and `membership.api_key` credentials to your [`ibexa.system..cdp` configuration](#configuration), using the credentials for [CDP API activation](#set-up-cdp-api-activation). To control for how long resolved segment memberships are cached per visitor, use the `ibexa_segmentation.anonymous.cache` configuration key: ```yaml # config/packages/ibexa_segmentation.yaml ibexa_segmentation: anonymous: cache: enabled: true # default; set to false to disable ttl: 300 # cache lifetime in seconds, default 300 (5 minutes) pool: 'ibexa.cache_pool' # Symfony cache pool service ID, default ibexa.cache_pool ``` - `enabled` - whether to cache CDP segment results per visitor cookie. Disabling this causes an additional API call to Raptor on every request - `ttl` - how long resolved segment results are cached per visitor (in seconds) - `pool` - the Symfony cache pool used to store the results # Export Raptor CDP data > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). Step-by-step data export procedure in Raptor CDP. Editions: Experience You need to specify a source of the user data that Raptor CDP connects to. To do so, go to **Data Manager** in **Tools** section and select **Create new dataflow**. It takes you to a Dataflow Creator, where in five steps you can set up a data streaming. ## General Information In the **General Information** section, specify dataflow name, choose **Stream File** as a source of user data and **CDP** as a destination, where they're sent for processing. Currently, only Stream File transport is supported and can be initialized from the configuration. ## Download In the **Download** section, select **Stream file**. Copy generated steam ID and paste it into the configuration file under `stream_id`. It allows you to establish a datastream from the Streaming API into the Data Manager. Next, you need to export your data to the CDP. Go to your installation and use this command: - for User: ```bash php bin/console ibexa:cdp:stream-user-data --draft ``` - for Product: ```bash php bin/console ibexa:cdp:stream-product-data --draft ``` - for Content: ```bash php bin/console ibexa:cdp:stream-content-data --draft ``` There are two versions of this command `--draft/--no-draft`. The first one is used to send the test user data to the Data Manager. If it passes a validation test in the **Activation** section, use the latter one to send a full version. You can extend exported user data with custom fields from your user content, such as date of birth, preferences, or other profile information. For more information, see [Data customization](https://doc.ibexa.co/en/saas/raptor_cdp/raptor_cdp_data_customization/#export-additional-user-data). Next, go back to Raptor CDP and select **Validate & download**. If the file passes, you can see a confirmation message. Now, you can go to the **File mapping** section. ## File mapping Mapping is completed automatically, the system fills all required information and shows available columns with data points on the right. You can change their names if needed or disallow empty fields by checking **Mandatory**. If the provided file contains empty values, this option isn't available. If provided file isn't recognized, the system requires you to fill in the parsing-options manually or select an appropriate format. If you make any alterations, select the **Parse File** to generate columns with new data. ## Transform & Map In the **Transform & Map** section you transform data and map it to a schema. At this point, you can map **email** to **email** and **id** to **integer** fields to get custom columns. If you have [extended user data export with custom fields](https://doc.ibexa.co/en/saas/raptor_cdp/raptor_cdp_data_customization/#export-additional-user-data), those fields appear as additional columns in this section. Make sure to add them to your schema in Raptor so they can be used for segmentation and recommendations. Next, select **Create schema based on the downloaded columns**. It moves you to Schema Creator. There, choose **PersonalData** as a parent and name the schema. ![Create new schema](https://doc.ibexa.co/en/saas/raptor_cdp/img/raptor_cdp_create_new_schema.png) Next, select all the columns and set Person Identifier as **userid**. ![Person Identifier](https://doc.ibexa.co/en/saas/raptor_cdp/img/raptor_cdp_person_identifier.png) If you used PersonData or Catalog type schemas, the system requires specifying the Write Mode that is applied to them. **Append** (default one) allows new data to overwrite the old one but leaves existing entries unaffected. All entries are stored in the dataset, unchanged by updating dataflow. For example, if a customer unsubscribes a newsletter, their email remains in the system. **Overwrite** completely removes the original dataset and replaces it with the new one every time the dataflow runs. Next, select **userid** from a **Schema columns section** on the right and map it to **id**. ![Map userid to id](https://doc.ibexa.co/en/saas/raptor_cdp/img/raptor_cdp_userid_mapid.png) ## Activation In this section you can test the dataflow with provided test user data. If everything passes, go to your installation and export production data with this command: ```bash php bin/console ibexa:cdp:stream-user-data --no-draft ``` Now you can run and activate the dataflow. ## Build new Audience/Segment Go to the **Audience Builder** and select **Build new audience**. When naming the audience remember, you need to find it in a drop-down list during activation. There, you can choose conditions from `did`, `did not` or `have`. The conditions `did` and `did not` allow you to use events like buy, visit or add to a cart from online tracking. - `have` conditions are tied to personal characteristics and can be used to track the sum of all buys or top-visited categories. In the Audience Builder, you can also connect created audiences to the activations. ## Activation Activation synchronises data from Raptor CDP to Cohesivo. When you specify a segment, you can activate it on multiple communication channels, such as newsletters or commercials. You can configure multiple activations based data flows. First, from the menu bar, select **Activations** and create a new **Ibexa** activation. Specify name of your activation, select `userid` as **Person Identifier** and click **Next**. ![General Information - Activation](https://doc.ibexa.co/en/saas/raptor_cdp/img/raptor_cdp_activation_general_info.png) Next, you can fill in **Ibexa information** they must match the ones provided in the YAML configuration: - **Client Secret** and **Client ID** - are used to authenticate against Webhook endpoint. In the configuration they're taken from environment variables in `.env` file. - **Segment Group Identifier** - identifier of the segment group in Cohesivo. It points to a segment group where all the CDP audiences are stored. - **Base URL** - URL of your instance with added `/cdp/webhook` at the end. ![Ibexa Information - Activation](https://doc.ibexa.co/en/saas/raptor_cdp/img/raptor_cdp_activation_ibexa_info.png) Finally, you can specify the audiences you wish to include. > **Note: CDP requests** > > All CDP requests are logged in with `debug` severity. ### Ibexa Messenger support for large batches of data CDP uses Ibexa Messenger to process incoming data from [Raptor](https://www.raptorservices.com/). This approach improves performance and reliability when processing large amounts of CDP user records. For more information, see [Background tasks: How it works](https://doc.ibexa.co/en/saas/infrastructure_and_maintenance/background_tasks/#how-it-works). By using Messenger while working with large batches of data, requests are queued instead of being processed synchronously: - queuing items starts automatically once a certain number of actions is reached (below this number, items are processed in a single request, using the standard synchronous behavior) - every single data is recorded in the database - a background worker retrieves records from the queue, processing them one by one or in batches, depending on the [Messenger](https://symfony.com/doc/7.4/messenger.html) configuration - processing happens at set intervals to avoid timeouts and keep the system stable 1. Make sure that the transport layer is [defined properly](https://doc.ibexa.co/en/saas/infrastructure_and_maintenance/background_tasks/#configure-package) in Ibexa Messenger configuration. 2. Add `bulk_async_threshold` setting in the `config/packages/ibexa_cdp.yaml` configuration: ```bash ibexa_cdp: bulk_async_threshold: 100 # Default: 100 items ``` Available options: - `bulk_async_threshold` (integer, default: 100) - minimum number of items required to trigger asynchronous processing - below threshold - items are processed immediately in a single request, using the standard synchronous behavior - at/above threshold - items are automatically dispatched to the asynchronous queue for background processing 3. Make sure that the [worker starts](https://doc.ibexa.co/en/saas/infrastructure_and_maintenance/background_tasks/#start-worker) together with the application to watch the transport bus: ```bash php bin/console messenger:consume ibexa.messenger.transport --bus=ibexa.messenger.bus ``` For more information, see [Start background task worker](https://doc.ibexa.co/en/saas/infrastructure_and_maintenance/background_tasks/#start-worker). ### CDP Monolog channel CDP Monolog channel handles webhook logs for easier separation of logs. ```bash - { name: monolog.logger, channel: ibexa.cdp.webhook } ``` It's possible to configure `ibexa.cdp.webhook` Monolog channel to direct all logs to specific stream, file, or service. This allows webhook logs to be stored separately from the main application logs for easier debugging and analysis. To do it, in `config/packages/monolog.yaml` file, define a new handler for the `ibexa.cdp.webhook` channel that directs CPD Webhook events to a separate file. It can be configured in both `dev` and `prod` environments, for example: ```yaml monolog: handlers: cdp_webhook: type: stream path: "%kernel.logs_dir%/cdp_webhook_%kernel.environment%.log" level: debug channels: [ 'ibexa.cdp.webhook' ] ``` If you want to avoid redundant or duplicate entries in the other logs, exclude the webhook channel by: ```yaml channels: ["!ibexa.cdp.webhook"] ``` # Track with Raptor CDP > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). Adding tracking in Raptor CDP. Editions: Experience The final step is setting up a tracking script that identifies visitors and records their interactions. You can set it up in two ways: - with Raptor's built in tracking functions - manually, with tracking scripts ## Set up tracking with built-in Raptor tracking functions If your project uses the [Raptor connector](https://doc.ibexa.co/en/saas/recommendations/raptor_integration/raptor_connector/index.md), use the built-in [Raptor tracking functions](https://doc.ibexa.co/en/saas/recommendations/raptor_integration/tracking_functions/index.md). This recommended approach supports both client-side and server-side tracking, handles cookie consent, and sets the tracking cookie required for [anonymous user segmentation](https://doc.ibexa.co/en/saas/raptor_cdp/raptor_cdp_activation/raptor_cdp_configuration/#anonymous-user-segmentation). For setup instructions, see [Raptor tracking functions](https://doc.ibexa.co/en/saas/recommendations/raptor_integration/tracking_functions/index.md). ## Manually set up tracking with tracking scripts If you aren't using the Raptor connector, you can set up tracking manually. It requires a head tracking script between the `` tags on your website, a main script after the head script, and cookie consent. For more information about setting up a tracking script manually, see [Raptor documentation](https://content.raptorservices.com/help-center/client-side-tracking). Now, you need to add a tracker to specific places in your website where you want to track users. For example, add this tracker to the landing page template to track various user activities: - user entrances ```js raptor.trackEvent('visit', ..., ...); ``` - user purchases ```js //Parameters for Product 1 raptor.trackEvent('buy', ..., ...); //Parameters for Product 2 raptor.trackEvent('buy', ..., ...); ``` For tracking to be effective, you also need to send ID of a logged-in user in the same way. Add the user ID information of logged-in users by using below script: ```js raptor.push("setRuid","USER_ID_HERE") ``` For anonymous visitors, Raptor's tracking script automatically sets an `rsa` cookie that uniquely identifies the visitor, without calling the `setRuid` method. For more information on tracking events, see [Raptor documentation](https://content.raptorservices.com/help-center/tracking-events-parameters-reference). # Schedule Raptor CDP data export > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). Data export schedule in Raptor CDP. Editions: Experience ## Configuration key Configuration in Raptor CDP allows you to automate the process of exporting content, users, and products. An `ibexa_cdp.data_export` [configuration key](https://doc.ibexa.co/en/saas/administration/configuration/configuration/#configuration-files) looks as below: ```yaml ibexa_cdp: data_export: schedule: user: - interval: '*/15 * * * *' options: '--stream-id=00000000-00000000-00000000-00000000 --user-content-type=user --no-draft' - interval: '0 */6 * * *' options: '--stream-id=00000000-00000000-00000000-00000000 --user-content-type=user --no-draft' content: - interval: '*/30 * * * *' options: '--stream-id=00000000-00000000-00000000-00000000 --content-type=article --no-draft' - interval: '0 */12 * * *' options: '--stream-id=00000000-00000000-00000000-00000000 --content-type=article --no-draft' product: - interval: '*/30 * * * *' options: '--stream-id=00000000-00000000-00000000-00000000 --product-type=computer --no-draft' - interval: '0 */12 * * *' options: '--stream-id=00000000-00000000-00000000-00000000 --product-type=computer --no-draft' ``` Under the `schedule` setting you can find separate sections for exporting user, content, and product. Structure of each section is exactly the same and includes `interval` and `options` elements: - `interval` - sets the frequency of the command invoke, for example, `'*/30 * * * *'` means "every 30 minutes", `'0 */12 * * *'` means "every 12th hour". It uses a standard `crontab` format, see [examples](https://crontab.guru/examples.html). - `options`- allows you to add arguments that have to be passed to the export command. This configuration allows you to provide multiple export workflows with parameters. It's important, because each type of content/product must have its own parameters on the CDP side, where each has a different Stream ID key and different required values, which are configured per data source. Accepted options can be listed with the command below: - for User: ```bash php bin/console ibexa:cdp:stream-user-data --help ``` - for Product: ```bash php bin/console ibexa:cdp:stream-product-data --help ``` - for Content: ```bash php bin/console ibexa:cdp:stream-content-data --help ``` The configuration is executed by `ibexa:cron:run` command which must be configured as a cron job. # Customize Raptor CDP data > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). Data customization in Raptor CDP. Editions: Experience You can customize user, content, and product data exported to CDP and you can control what field type information you want to export. By default, custom field types have basic export functionality. It casts their `Value` object to string, thanks to `\Stringable` implementation. ## Export additional user data You can extend user data exported to CDP by attaching custom information, for example user content fields or user preferences. Use it for advanced customer segmentation and recommendations in marketing campaigns. To add custom data to user exports, create a class that extends [`\Ibexa\Contracts\Cdp\Export\User\AbstractUserItemProcessor`](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-Cdp-Export-User-AbstractUserItemProcessor.html) and implement the `doProcess()` method. The base class handles user field validation and provides helper methods for working with user content. The following example adds a custom date of birth field to the exported data: ```php getUserField($userContent); if (null === $userField) { throw new InvalidArgumentException('Content does not contain user field'); } $dateOfBirth = ''; $dateOfBirthField = $userContent->getField($this->dateOfBirthFieldIdentifier); if ($dateOfBirthField !== null && $dateOfBirthField->value instanceof DateValue && $dateOfBirthField->value->date !== null ) { $dateOfBirth = $dateOfBirthField->value->date->format('Y-m-d'); } return array_merge( $processedItemData, [ 'date_of_birth' => $dateOfBirth, ] ); } } ``` Register your processor as a Symfony service and tag it with `ibexa.cdp.export.user.item_processor`: ```yaml services: App\Export\User\DateOfBirthUserItemProcessor: parent: Ibexa\Contracts\Cdp\Export\User\AbstractUserItemProcessor arguments: $dateOfBirthFieldIdentifier: 'date_of_birth' tags: - { name: 'ibexa.cdp.export.user.item_processor', priority: 100 } ``` The `priority` parameter controls the order of execution when multiple processors are registered. Higher priority values run first. Your custom processor can modify the data returned from the previous processors, for example by adding new entries or modifying the existing ones. The exported user data includes your custom fields: ```json { "date_of_birth": "2000-01-01", "id": 1, "login": "example", "email": "example@example.org", "name": "John Doe", } ``` ## Export field types Field types are exported with metadata, for example, ID, field definition name, type, or value. You can also provide your own [`\Ibexa\Contracts\Cdp\Export\Content\FieldProcessorInterface`](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-Cdp-Export-Content-FieldProcessorInterface.html) instance to extend metadata. The provided implementation has to be defined as a service and tagged with `ibexa.cdp.export.content.field_processor`. Additionally, you can specify `priority` to override the default behavior. All system Field Processors use `-100` priority, and any higher priority value overrides them. The interface is plain and has two methods that you need to provide: - **supports** - decides whether your `FieldProcessor` can work with the `Field` instance. - **process** - takes `Field` instance and then returns a flat array of scalar values that are combined with the payload data. ​ A common field type is serialized to: ```json { "field_measurement_simple_id": 1792, "field_measurement_simple_type": "ibexa_measurement", "field_measurement_simple_language_code": "eng-GB", "field_measurement_simple_value_measurement": "data transfer rate", "field_measurement_simple_value_unit_identifier": "megabyte per second", "field_measurement_simple_value_unit_symbol": "MB/s", "field_measurement_simple_value_unit_is_base": false, "field_measurement_simple_value_base_unit_identifier": "bit per second", "field_measurement_simple_value_base_unit_symbol": "bit/s", "field_measurement_simple_value_simple": 100, "field_measurement_simple_value_simple_base_unit": 800000000 } ``` Field identifier is a prefix that is automatically added to each key. You can only use scalar values. ### Built in Field Processors for custom field types You can provide your own CDP export functionality by using one of the system Field Processors: #### `\Ibexa\Cdp\Export\Content\FieldProcessor\SkippingFieldProcessor` It results in the field type being excluded from the exported payload. To avoid adding the field type data to the payload, register a new service as follows: ​ ```yaml custom_fieldtype.cdp.export.field_processor: class: Ibexa\Cdp\Export\Content\FieldProcessor\SkippingFieldProcessor autoconfigure: false arguments: $fieldTypeIdentifier: custom_fieldtype tags: - { name: 'ibexa.cdp.export.content.field_processor', priority: 0 } ``` ## Export field type values To customize export of field type values, provide your own [`\Ibexa\Contracts\Cdp\Export\Content\FieldValueProcessorInterface`](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-Cdp-Export-Content-FieldValueProcessorInterface.html) instance. New implementation has to be registered as a service manually or by using autoconfiguration. The service has to use the tag `ibexa.cdp.export.content.field_value_processor`. You can also provide `priority` property to override other Field Value Processors. - `FieldValueProcessorInterface::process` - takes `Field` instance and returns an `array` with scalar values that are applied to export data payload. If the field type returns a single value, provides a `value` key in the array. You can return multiple values. - `FieldValueProcessorInterface::supports` - decides whether `FieldValueProcessor` can work with the `Field`. ​ ### Built in Field Value Processors for custom field types Several system Field Value Processors either work by default or can be registered for custom field types: #### `\Ibexa\Cdp\Export\Content\FieldValueProcessor\CastToStringFieldValueProcessor` This Processor is a default one, as long as no other Processor with higher priority is registered. It makes `\Stringable` implementation of the field type `\Ibexa\Core\FieldType\Value` object to use it as a value in the final payload. #### `\Ibexa\Cdp\Export\Content\FieldValueProcessor\JsonHashFieldValueProcessor` This Processor generates JSON data from hash representation of the field type (it uses [`\Ibexa\Contracts\Core\FieldType\FieldType::toHash`](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-Core-FieldType-FieldType.html#method_toHash) method). > **Caution: Caution** > > CDP doesn't support column mapping, which allows you to match records on JSON data directly. To use `JsonHashFieldValueProcessor`, you need to register a new service: ​ ```yaml custom_fieldtype.cdp.export.field_processor: class: Ibexa\Cdp\Export\Content\FieldValueProcessor\JsonHashFieldValueProcessor autoconfigure: false arguments: $fieldTypeIdentifier: custom_fieldtype tags: - { name: 'ibexa.cdp.export.content.field_value_processor', priority: 0 } ``` # Search # Search > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). Cohesivo search functionalities allow working with three search engines and using search API to run complex and precise queries about content and products. Cohesivo exposes a very powerful [Search API](https://doc.ibexa.co/en/saas/search/search_api/index.md), allowing both full-text search and querying the content repository by using several built-in Search Criteria and Sort Clauses. These are supported across different search engines, allowing you to plug in another search engine without changing your code. - [Search engines](https://ez-systems-developer-documentation--3381.com.readthedocs.build/en/3381/search/search_engines/search_engines/): Learn about different search engines that are supported by Cohesivo. - [Elasticsearch search engine](https://ez-systems-developer-documentation--3381.com.readthedocs.build/en/3381/search/search_engines/elasticsearch/elasticsearch_overview/): Elasticsearch search engine overview. - [Solr search engine](https://ez-systems-developer-documentation--3381.com.readthedocs.build/en/3381/search/search_engines/solr_search_engine/solr_overview/): Solr search engine overview. - [Search API](https://ez-systems-developer-documentation--3381.com.readthedocs.build/en/3381/search/search_api/): You can search for content, locations and products by using the PHP API. Fine-tune the search with Search Criteria, Sort Clauses and Aggregations. - [Search Criteria and Sort Clauses](https://ez-systems-developer-documentation--3381.com.readthedocs.build/en/3381/search/search_criteria_and_sort_clauses/): Search Criteria and Sort Clauses help you fine-tune searches done by using the Search API. - [Create custom Search Criterion](https://ez-systems-developer-documentation--3381.com.readthedocs.build/en/3381/search/extensibility/create_custom_search_criterion/): Create custom Search Criterion to use with Solr and Elasticsearch search engines. - [Create custom Sort Clause](https://ez-systems-developer-documentation--3381.com.readthedocs.build/en/3381/search/extensibility/create_custom_sort_clause/): Create custom Sort Clause to use with Solr and Elasticsearch search engines. - [Create custom Aggregation](https://ez-systems-developer-documentation--3381.com.readthedocs.build/en/3381/search/extensibility/create_custom_aggregation/): Create custom Aggregation to use with Solr and Elasticsearch search engines. # Search engines > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). Learn about different search engines that are supported by Cohesivo. Cohesivo enables you to use different search engines. Currently, they exist in their own Cohesivo Bundles: 1. [Legacy search engine](https://doc.ibexa.co/en/saas/search/search_engines/legacy_search_engine/legacy_search_overview/index.md) - a database-powered search engine for basic needs. 2. [Solr](https://doc.ibexa.co/en/saas/search/search_engines/solr_search_engine/solr_overview/index.md) - an integration providing better overall performance, better scalability and support for more advanced search capabilities. 3. [Elasticsearch](https://doc.ibexa.co/en/saas/search/search_engines/elasticsearch/elasticsearch_overview/index.md) - a document-oriented engine providing even better performance and scalability. ## Search engines comparison | Feature | Legacy Search Engine (SQL) | Solr | Elasticsearch | | --------------------------- | -------------------------- | ---- | ------------------------- | | Filtering | Yes, limited\* | Yes | Yes | | Query (filter with scoring) | Only filters, no scoring | Yes | Yes | | Full-text search | Yes, limited\*\* | Yes | Yes, limited | | Index-time boosting | No | No | Query-time boosting\*\*\* | | Aggregations | No | Yes | Yes | \* Usage of Criteria and Sort Clauses for fields doesn't perform well on medium to larger amount of data with Legacy Search Engine (SQL). \*\* For more information about full-text search syntax support, see [Full-text Criterion](https://doc.ibexa.co/en/saas/search/criteria_reference/fulltext_criterion/index.md). \*\*\* Elasticsearch offers query-time boosting instead. # Elasticsearch search engine > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). Elasticsearch search engine overview. Elasticsearch is an open-source, distributed, Java-based search engine that responds to queries in real-time and is scalable in reaction to changing processing needs. Elasticsearch enables you to use filtering, query, query-time boosting, full-text search, and aggregations. It organizes data into documents, that then are grouped into indices. As a result of having distributed architecture, Elasticsearch can analyze massive amounts of data with almost real-time performance. Instead of searching text directly, it searches and index. Thanks to this mechanism, it's able to achieve fast response. For a detailed description of advanced settings that you might require in a specific production environment, see the documentation provided by Elastic. Start with the [Set up Elasticsearch](https://www.elastic.co/guide/en/elasticsearch/reference/8.19/setup.html) section. ## Prerequisite To proceed you need to be familiar with how indexing, filtering and queries work. ## Update Elasticsearch schema Whenever you make any changes in case of variables (for example, environmental ones) or configuration files, you need to erase Elasticsearch index, update the schema, and rebuild the index. To delete an index, you can use the Elasticsearch's REST API. First, use the [`_cat/indices` endpoint](https://www.elastic.co/guide/en/elasticsearch/reference/8.19/cat-indices.html) to list existing indices. For example, the command `curl -H "Accept: application/text" elasticsearch:9200/_cat/indices` returns output like the following: ```bash yellow open default_location_eng_gb_54 DoSFV-CtQFylKKVvd48YfA 1 1 1 0 16.7kb 16.7kb yellow open default_location_eng_gb_42 3Z_IrWVHQh2m37jPqQBOcQ 1 1 1 0 20.1kb 20.1kb yellow open default_content_eng_gb_45 y-t4uNQwR4KRJ-N9i3zUog 1 1 1 0 21.3kb 21.3kb yellow open default_content_eng_gb_46 e_LS5qG3RIih6iQRPsNp-w 1 1 1 0 22.5kb 22.5kb yellow open default_content_eng_gb_1 101-1-tQS_2KSvNs2X2JAQ 1 1 17 0 39.8kb 39.8kb yellow open default_location_eng_gb_46 fSGtpljwTpGfascFechmww 1 1 1 0 21kb 21kb (...) ``` Create a list containing all indices used by Cohesivo, including the [custom indices](https://doc.ibexa.co/en/saas/search/search_engines/elasticsearch/configure_elasticsearch/#define-field-type-mapping-templates) as well. Then, delete them by using the [delete index endpoint](https://www.elastic.co/guide/en/elasticsearch/reference/8.19/indices-delete-index.html) ```bash curl --request DELETE 'https://elasticsearch:9200/default_location*' curl --request DELETE 'https://elasticsearch:9200/default_content*' (...) ``` > **Tip: Tip** > > To quickly delete all existing Elasticsearch indices, you can use the `_all` keyword as the name of the index, as in the following request: `curl --request DELETE https://elasticsearch:9200/_all`. Always review the list of existing indices and confirm they are safe to delete before executing this command, as it permanently removes data. To update the schema and then reindex the search, use the following commands: ```bash php bin/console ibexa:elasticsearch:put-index-template --overwrite php bin/console ibexa:reindex ``` # Configure Elasticsearch > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). Configure Elasticsearch to use it with Cohesivo. ## Configure connections To configure Elasticsearch, first, you need to configure the connections. There are two possibilities of connection: - using [cluster of Elasticsearch nodes](#configure-clustering) - using [Elasticsearch Cloud](#configure-elasticsearch-cloud) No matter which option you choose, you have to define the connection settings under the `connections` key. Set a name of the connection: ```yaml ibexa_elasticsearch: connections: : ``` > **Tip: A default connection** > > If you define more than one connection, for example, to create a separate connection for each repository, you must select the one that Cohesivo should use with the following setting: > > ```yaml > ibexa_elasticsearch: > # ... > default_connection: > ``` Now, you need to decide whether to add a cluster that you administer and manage yourself, or use a cloud solution from Elastic, and configure additional parameters. If you want to connect by using a cluster, follow the instructions below in the [Cluster](#configure-clustering) section. If you want to use Elasticsearch Cloud, skip to [Elasticsearch Cloud](#configure-elasticsearch-cloud) section. ## Configure clustering A cluster consists of nodes. You might start with one node and then add more nodes if you need more processing power. When you configure a node, you need to set the following parameters: - `host` - an IP address or domain name of the host. Default value: `localhost`. - `port` - a port to connect to. Default value: `9200`. If you have several Elasticsearch instances that run on the same host, and want to make them distinct, you can change the default number. - `scheme` - a protocol used to access the node. Default value: `http`. - `path` - by default, path isn't used. Default value: `null`. If you have several Elasticsearch instances that run on the same host, and want to make them distinct, you can define a path for each instance. - `user`/`pass` - credentials, if needed to log in to the host. Default values: `null`. Next, list the addresses of cluster nodes under the `hosts` key: ```yaml ibexa_elasticsearch: connections: : hosts: - '127.0.0.1:9200' # ... ``` There are several ways that you can use to pass host parameters. The easiest one is to pass them as a string: ```yaml - https://:9200// ``` You can also pass the host configuration as an object that lists parameter-value pairs, for example, when your authentication settings contain special characters. ```yaml - { host: '', scheme: 'http', port: 9200, path: '/', user: , pass: } ``` Cluster connection configuration should have the following structure: ```yaml ibexa_elasticsearch: connections: simple: hosts: - '127.0.0.1:9200' - '127.0.0.1:9201' - '127.0.0.1:9202' localhost: debug: true hosts: - "127.0.0.1:9200" - "b.elasticsearch.loc:9200" - "c.elasticsearch.loc:9200" intranet: debug: true hosts: - "c.elasticsearch.loc:9200" default_connection: simple ``` ### Multi-node cluster behavior When you configure a cluster-based connection, and the cluster consists of many nodes, you can choose strategies that govern how the cluster reacts to changing operating conditions, or how workload is distributed among the nodes. #### Node pool settings With these settings you decide how nodes in the cluster are selected and how failed nodes are resurrected. The node pool manages the list of active nodes, which can change over time due to connectivity issues, host malfunction, or when you add new nodes to the cluster to increase performance. By default, Elasticsearch uses the `SimpleNodePool` algorithm with `RoundRobin` selector and `NoResurrect` strategy. You can customize the node pool behavior with the following settings: ```yaml : # ... node_pool_selector: Elastic\Transport\NodePool\Selector\RoundRobin node_pool_resurrect: Elastic\Transport\NodePool\Resurrect\NoResurrect ``` For more information and a list of available choices, see [Node pool](https://www.elastic.co/guide/en/elasticsearch/client/php-api/8.19/node_pool.html). > **Tip: Load tests recommendation** > > If you change the node pool settings, it's recommended that you perform load tests to check whether the change doesn't negatively impact the performance of your environment. ##### Number of retries The `retries` setting configures the number of attempts that Cohesivo makes to connect to the nodes of the cluster before it throws an exception. By default, `null` is used, which means that the number of retries equals to the number of nodes in the cluster. ```yaml : # ... retries: null ``` Depending on the node pool settings that you select, Cohesivo's reaction to reaching the maximum number of retries might differ. For more information, see [Set retries](https://www.elastic.co/guide/en/elasticsearch/client/php-api/8.19/set-retries.html). ## Configure Elasticsearch Cloud As an alternative to using your own cluster, you can use Elasticsearch Cloud, a commercial SaaS solution. With Elasticsearch Cloud you don't have to build or manage your own Elasticsearch cluster. Also, you do all the configuration and administration in a graphical user interface. To connect to a cloud solution with Cohesivo, you must set the `elastic_cloud_id` parameter by providing an alphanumerical ID string that you get from the cloud's user interface, for example: ```yaml : elastic_cloud_id: 'production:ZWFzdHVzMi5henVyZS5lbGFzdGljLWNsb3VkLmNvbTo5MjQzJGUwZ' ``` With the ID set, you must configure authentication to be able to access the remote environment. ## Configure security Elasticsearch instances support `basic` and `api_key` authentication methods. You select authentication type and configure the settings under the `authentication` key. By default, authentication is disabled: ```yaml : # ... authentication: type: null ``` If you connect to Elasticsearch hosts outside of your local network, you might also need to configure SSL encryption. ### Basic authentication If your Elasticsearch server is protected by HTTP authentication, you must provide Cohesivo with the credentials. In the basic authentication, you must pass the following parameters: ```yaml : # ... authentication: type: basic credentials: [''] ``` For example: ```yaml ibexa_elasticsearch: connections: cloud: debug: true elastic_cloud_id: 'test:ZWFzdHVzMi5henVyZS5lbGFzdGljLWNsb3VkLmNvbTo5MjQzJGUwZ' authentication: type: basic credentials: ['elastic', '1htFY83VvX2JRDw88MOkOejk'] ``` ### API key authentication If your Elasticsearch cluster is protected by API keys, you must provide the key and secret in authentication configuration to connect Cohesivo with the cluster. With API key authentication you can define different authorization levels, such as [`create_index` or `index`](https://www.elastic.co/guide/en/elasticsearch/reference/8.19/security-privileges.html#privileges-list-indices). Such approach proves useful if the cluster is available to the public. For more information, see [Create API key](https://www.elastic.co/guide/en/elasticsearch/reference/8.19/security-api-create-api-key.html). When using API key authentication, you must pass the following parameters to authenticate access to the cluster: ```yaml : # ... authentication: type: api_key credentials: ['', ''] ``` For example: ```yaml ibexa_elasticsearch: connections: cloud: debug: true elastic_cloud_id: 'test:ZWFzdHVzMi5henVyZS5lbGFzdGljLWNsb3VkLmNvbTo5MjQzJGUwZ' authentication: type: api_key credentials: ['ui2lp2axTNmsyakw9tvNnw', 'VuaCfGcBCdbkQm-e5aOx'] ``` Alternatively, pass the encoded API key value, which Elasticsearch also calls "API key credentials": ```yaml : # ... authentication: type: api_key credentials: [''] ``` For example: ```yaml ibexa_elasticsearch: connections: cloud: debug: true elastic_cloud_id: 'test:ZWFzdHVzMi5henVyZS5lbGFzdGljLWNsb3VkLmNvbTo5MjQzJGUwZ' authentication: type: api_key credentials: ['VnVhQ2ZHY0JDZGJrUW0tZTVhT3g6dWkybHAyYXhUTm1zeWFrdzl0dk5udw=='] ``` To see the difference between API key, API key id, and encoded API key, refer to the [examples in Elasticsearch documentation](https://www.elastic.co/guide/en/elasticsearch/reference/8.19/security-api-create-api-key.html#security-api-create-api-key-example). ### SSL When you need to protect your communication with the Elasticsearch server, you can use SSL encryption. When configuring SSL for your internal infrastructure, you can use your own client certificates signed by a public CA. Configure SSL by passing the path-passwords pairs for both the certificate and the certificate key. For example: ```yaml ibexa_elasticsearch: connections: cloud_with_ssl: debug: true elastic_cloud_id: 'test:ZWFzdHVzMi5henVyZS5lbGFzdGljLWNsb3VkLmNvbTo5MjQzJGUwZ' authentication: type: api_key credentials: ['8Ek5f3IBGQlWj6v4M7zG', 'rmI6IechSnSJymWJ4LZqUw'] ssl: cert: path: '/path/to/cert.pem' pass: ~ cert_key: path: '/path/to/cert-key' pass: ~ ``` If you don't have a client certificate signed by public certificate authority, but you have a self-signed CA certificate generated by `elasticsearch-certutil` or another tool (for example for development purposes), use the following `ssl` configuration: ```yaml ibexa_elasticsearch: connections: cloud_with_ssl: debug: true elastic_cloud_id: 'test:ZWFzdHVzMi5henVyZS5lbGFzdGljLWNsb3VkLmNvbTo5MjQzJGUwZ' ssl: ca_cert: path: '/path/to/ca_cert.pem' ``` If you configure both `ca_cert` and `cert` entries, the `ca_cert` parameter takes precedence over the `cert` parameter. After you have configured SSL, you can still disable it, for example when the certificates expire, or you're migrating to a new set of certificates. To do this, pass the following setting under the `ssl` key: ```yaml verification: false ``` For more information, see [Elasticsearch: Security by default](https://www.elastic.co/guide/en/elasticsearch/client/php-api/8.19/connecting.html#auth-http). ### Enable debugging In a staging environment, you can log messages about the status of communication with Elasticsearch. You can then use Symfony Profiler to review the logs. By default, debugging is disabled. To enable debugging, you can use the following setting: ```yaml : # ... debug: ``` - `debug` logs information about requests, including request status and timing > **Note: Elasticsearch 7 compatibility** > > If you're using Elasticsearch 7, you can also use the `trace` setting for additional debugging information. This setting is deprecated and removed in Elasticsearch 8. > **Tip: Tip** > > Make sure that you disable debugging in a production environment. ## Define field type mapping templates Before you can re-index the Cohesivo data, so that Elasticsearch can search through its contents, you must define an index template. Templates instruct Elasticsearch to recognize Cohesivo fields as specific data types, based on, for example, a field name. They help you prevent Elasticsearch from using the dynamic field mapping feature to create type mappings automatically. You can create several field type mapping templates for each index, for example, to define settings that are specific for different languages. When you establish a relationship between a field mapping template and a connection, you can apply several templates, too. ### Define a template To define a field mapping template, you must provide settings under the `index_templates` key. The structure of the template is as follows: ```yaml ibexa_elasticsearch: # ... index_templates: : patterns: # ... settings: # ... mappings: # ... ``` Set a unique name for the template and configure the following keys: - `patterns` - A list of wildcards that Elasticsearch uses to match the field mapping template to an index. Index names use the following pattern: `___` By default, repository name is set to `default`, however, in the context of a Cohesivo instance, there can be [several repositories with different names](https://doc.ibexa.co/en/saas/administration/configuration/repository_configuration/#defining-custom-connection). Document type can be either `content` or `location`. In a language code, hyphens are replaced with underscores, and all characters must be lowercase. An index name can therefore look like this: `default_content_eng_gb_2` You can use the `patterns` setting when your data contains content in different languages. You can create index templates with settings that apply to a specific language only, for example, to eliminate stop words from the index, or help divide concatenations. You use patterns to identify index templates that contain settings specific for a given language: ```yaml ibexa_elasticsearch: # ... index_templates: default_en_us: patterns: ['default_*', '*eng_us*'] # ... ``` - `settings` - Settings under this key control all aspects related to an index. For more information and a list of available settings, see [Elasticsearch documentation](https://www.elastic.co/guide/en/elasticsearch/reference/8.19/index-modules.html#index-modules-settings). ```text For example, you can define settings that convert text into a format that is optimized for search, like a normalizer that changes a case of all phrases in the index: ``` ```yaml ibexa_elasticsearch: # ... index_templates: default: # ... settings: analysis: normalizer: lowercase_normalizer: type: custom char_filter: [] filter: lowercase # ... ``` - `mappings` - Settings under this key define mapping for fields in the index. For more information about mappings, see [Elasticsearch documentation](https://www.elastic.co/guide/en/elasticsearch/reference/8.19/mapping.html). ```text When you create a custom index template, with settings for your own field and document types, make sure that it contains mappings for all searchable fields that are available in Cohesivo. ``` To see the default configuration, go to `vendor/ibexa/elasticsearch/src/bundle/Resources/config/` and open the `default-config.yaml` file. ### Fine-tune the search results Your search results can be adjusted by configuring additional parameters. For a list of available mapping parameters and their usage, see [Elasticsearch documentation](https://www.elastic.co/guide/en/elasticsearch/reference/8.19/mapping-params.html). For example, you can apply a mapping parameter, in this case, a normalizer, to a specific mapping under the `dynamic_templates` key: ```yaml ibexa_elasticsearch: # ... index_templates: default: # ... mappings: # ... dynamic_templates: - ez_string: match: "*_s" mapping: type: keyword normalizer: lowercase_normalizer # ... ``` You can also set a boosting factor for a specific field. Boosting increases the relevance of hits, for example making keywords from the title more relevant than the ones from other places of the document. Set the boosting factor under the `properties` key: ```yaml ibexa_elasticsearch: # ... index_templates: default: # ... mappings: properties: content_name_s: boost: 2.0 # ... ``` You can even copy contents of existing fields, process them and then paste into another field, which than can be queried: ```yaml ibexa_elasticsearch: # ... index_templates: default: # ... mappings: properties: user_first_name_s: type: keyword normalizer: lowercase_normalizer copy_to: custom_field # ... ``` ### Add language-specific analysers You can configure Elasticsearch to perform language-specific analysis like stemming. This way searching for "cars" returns hits with content that contains the word "car". On a multilingual site, you can have different analyzers configured for different languages, something which is typically required because stemming rules are language-specific. #### Make a copy of the default template To enable a language-specific analyzer, create a new template for each language in `config/packages/ibexa_elasticsearch.yaml` first. This template should be based on the `default` template found in `vendor/ibexa/elasticsearch/src/bundle/Resources/config/default-config.yaml`. The name of the new template should indicate the language it applies to, for example `eng_gb`, `nor_no` or `fre_fr`. #### Change match pattern for the new template The default template matches on `*_location_*` and `*_content_*`. These patterns aren't language-specific and you cannot use them if you plan to use different templates for different languages. In your copy of the default template, change the pattern as follows: ```diff patterns: - - '*_location_*' - - '*_content_*' + - "*_eng_gb*" ``` This pattern matches on English. For more information about specifying the pattern for your language, see [Define a template](#define-a-template). #### Create config for language specific analyzer For information about configuring an analyzer for each specific language, see [Elasticsearch documentation](https://www.elastic.co/guide/en/elasticsearch/reference/8.19/analysis-lang-analyzer.html). An adoption of the [`english` analyzer](https://www.elastic.co/guide/en/elasticsearch/reference/8.19/analysis-lang-analyzer.html#english-analyzer) in Cohesivo configuration looks like this: ```yaml ibexa_elasticsearch: index_templates: english: patterns: - '*_eng_gb*' settings: analysis: normalizer: lowercase_normalizer: type: custom char_filter: [] filter: - lowercase analyzer: english_analyzer: type: custom tokenizer: lowercase filter: - lowercase - english_stop - english_keywords - english_stemmer - english_possessive_stemmer ibexa_spellcheck_analyzer: type: custom tokenizer: lowercase filter: - lowercase - ibexa_spellcheck_shingle_filter ibexa_spellcheck_raw_analyzer: type: custom tokenizer: standard filter: - lowercase - english_possessive_stemmer filter: ibexa_spellcheck_shingle_filter: type: shingle min_shingle_size: 2 max_shingle_size: 3 english_stop: type: stop stopwords: '_english_' english_keywords: type: keyword_marker keywords: [] english_stemmer: type: stemmer language: light_english english_possessive_stemmer: type: stemmer language: possessive_english refresh_interval: "-1" mappings: dynamic_templates: - ez_int: match: "*_i" mapping: type: integer - ez_mint: match: "*_mi" mapping: type: integer - ez_id: match: "*_id" mapping: type: keyword - ez_mid: match: "*_mid" mapping: type: keyword - ez_string: match: "*_s" mapping: type: keyword normalizer: lowercase_normalizer - ez_mstring: match: "*_ms" mapping: type: keyword normalizer: lowercase_normalizer - ez_long: match: "*_l" mapping: type: long - ez_mlong: match: "*_ml" mapping: type: long - ez_text: match: "*_t" mapping: type: text analyzer: english_analyzer - ez_text_fulltext: match: "*_fulltext" mapping: type: text analyzer: english_analyzer - ez_boolean: match: "*_b" mapping: type: boolean - ez_mboolean: match: "*_mb" mapping: type: boolean - ez_float: match: "*_f" mapping: type: float - ez_double: match: "*_d" mapping: type: double - ez_date: match: "*_dt" mapping: type: date - ez_geolocation: match: "*_gl" mapping: type: geo_point - ez_spellcheck: match: "*_spellcheck" mapping: type: text analyzer: ibexa_spellcheck_analyzer fields: raw: type: text analyzer: ibexa_spellcheck_raw_analyzer ``` Then, you must bind this language template to your Elasticsearch connection. ## Bind templates with connections After you create an index template (for example, for specific data types or linguistic analysis), you must link it to an Elasticsearch connection by adding the `index_templates` key to the connection definition. If your configuration file contains several connection definitions, you can reuse the same template for different connections. If you have several index templates, you can apply different combinations of templates to different connections. ```yaml ibexa_elasticsearch: connections: : # ... index_templates: - eng_gb : # ... index_templates: - eng_gb - fre_fr - ger_de ``` For more information about how Elasticsearch handles settings and mappings from multiple templates that match the same index, see [Elasticsearch documentation](https://www.elastic.co/guide/en/elasticsearch/reference/8.19/index-templates.html). ## Extend Elasticsearch To learn how you can create document field mappers, custom Search Criteria, custom Sort Clauses and Aggregations, see [Create custom Search Criterion](https://doc.ibexa.co/en/saas/search/extensibility/create_custom_search_criterion/index.md). # Solr search engine > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). Solr search engine overview. [Solr search engine](https://github.com/ibexa/solr) allows you to use advanced search features: filtering, query, full-text search, and aggregations. When you enable Solr and re-index your content, all your existing Search queries by using `SearchService` are powered by Solr automatically. This allows you to scale up your Cohesivo installation and be able to continue development locally against SQL engine, and have a test infrastructure, Staging, and Prod powered by Solr. By this, it also removes considerable load from your database. For more information on the architecture of Cohesivo, see [Architecture](https://doc.ibexa.co/en/saas/administration/project_organization/architecture/index.md). # Configure Solr > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). Configure Solr search engine to use it with Cohesivo. ## Configure boosting > **Caution: Index time boosting** > > Index time boosting was deprecated in Solr 6.5 and removed in Solr 7.0. Until query time boosting is implemented, there is no way to boost in the bundle out of the box. > **Tip: How boosting interacts with Search API** > > Boosting of fields or documents affects the score (relevance) of your search result hits when using Search API for any Criteria you specify on `$query->query`, or in REST by using `Query` element. When you don't specify anything to sort on, the result is sorted by this relevance. Anything set on `$query->filter`, or in REST by using `Filter` element, *doesn't* affect scoring and only works as a pure filter for the result. Thus make sure to place Criteria you want to affect scoring on `query`. Boosting currently happens when indexing, so if you change your configuration you need to re-index. Boosting tells the search engine which parts of the content model have more importance when searching, and is an important part of tuning your search results relevance. Importance is defined by using a numeric value, where `1.0` is default, values higher than that are more important, and values lower (down to `0.0`) are less important. Boosting is configured per connection that you configure to use for a given repository, like in this `config/packages/ibexa_solr.yaml` example: ```yaml ibexa_solr: connections: default: boost_factors: content_type: # Boost a whole content type article: 2.0 meta_field: # Boost a meta Field (name, text) system wide, or for a given content type name: 10.0 article: # Boost the meta full text Field for article more than 2.0 set above text: 5.0 ``` The configuration above results in the following boosting (content type / Field): - `article/title: 2.0` - `news/description: 1.0` (default) - `article/text (meta): 5.0` - `blog_post/name (meta): 10.0` - `article/name (meta): 2.0` > **Tip: How to configure boosting on specific fields** > > Currently, boosting on particular fields is missing. However, it could be configured using 3rd party [Novactive/NovaeZSolrSearchExtraBundle](https://github.com/Novactive/NovaeZSolrSearchExtraBundle) in case of custom search implementation, for example, to handle your front-end search form. Unfortunately, this doesn't affect search performed in the administration interface. > > The following example presents boosting configuration for Folder's `name` and `description` fields. First, in `ibexa_solr.yaml` configure [custom fulltext fields](https://github.com/Novactive/NovaeZSolrSearchExtraBundle/blob/master/doc/custom_fields.md). > > ```yaml > ez_solr_search_extra: > system: > default: > fulltext_fields: > custom_folder_name: > - folder/name > custom_folder_description: > - folder/description > ``` > > The second step requires you to use `\Novactive\EzSolrSearchExtra\Query\Content\Criterion\MultipleFieldsFullText` instead of default `\Ibexa\Contracts\Core\Repository\Values\Content\Query\Criterion\FullText`. The following example shows custom query which benefits from the custom fields created in the previous example. > > ```php > > namespace App\Controller; > > use Ibexa\Contracts\Core\Repository\SearchService; > use Ibexa\Contracts\Core\Repository\Values\Content\Query; > use Symfony\Component\HttpFoundation\Request; > use Symfony\Component\HttpFoundation\Response; > > class SearchController > { > /** > * @var \Ibexa\Contracts\Core\Repository\SearchService > */ > private $searchService; > > public function __construct(SearchService $searchService) > { > $this->searchService = $searchService; > } > > public function searchAction(Request $request): Response > { > $queryString = $request->get('query'); > > $query = new Query(); > $query->query = new \Novactive\EzSolrSearchExtra\Query\Content\Criterion\MultipleFieldsFullText( > $queryString, > [ > 'metaBoost' => [ > 'custom_folder_name' => 20.0, > 'custom_folder_description' => 10.0, > ] > ] > ); > > $searchResult = $this->searchService->findContent($query); > > ... > } > } > ``` > > Remember to clear the cache and perform search engine reindex afterwords. > > The above configuration results in the following boosting (content type / field): > > - `folder/name: 20.0` > - `folder/description: 10.0` ## Index related objects You can use indexation of related objects to search through text of related content. Indexing is disabled by default. To set it up you need to define the maximum indexing depth using the following YAML configuration: ```yaml ibexa_solr: # ... connections: default: # ... indexing_depth: # Default value: 0 - no relation indexing, 1 - direct relations, 2nd level relations, 3rd level relations (maximum value). default: 1 content_type: # Index depth defined for specific content type article: 2 ``` ## Configure Solr Replication (master/slave) > **Note: Note** > > The configuration below has been tested on Solr 7.7. ### Configure Master for replication First you need to change the core configuration in `solrconfig.xml` (for example `*/opt/solr/server/ibexa/collection1/conf/solrconfig.xml`). You can copy and paste the code below before any other `requestHandler` section. ```xml optimize optimize schema.xml,stopwords.txt,elevate.xml 00:00:10 2 16 solrconfig_slave.xml:solrconfig.xml,x.xml,y.xml ``` Then restart the master with: ```bash sudo su - solr -c "/opt/solr/bin/solr restart" ``` ### Configure Slave for replication You have to edit the same file on the slave server, and use the code below: ```xml http://123.456.789.0:8983/solr/collection1/replication 00:00:20 internal 5000 10000 username password ``` Next, restart Solr slave. Connect to the Solr slave interface (), go to your core and check the replication status: ![Solr Slave](https://doc.ibexa.co/en/saas/search/img/solr.png) ## Configure HTTP Client for Solr queries Cohesivo Solr Bundle uses Symfony HTTP Client to fetch and update Solr index. You can configure timeout and maximum number of retries for that client using Solr Bundle's Semantic configuration: ```yaml ibexa_solr: # ... http_client: # ... timeout: 30 max_retries: 5 ``` ## Extend Solr To learn how you can create document field mappers, custom Search Criteria, custom Sort Clauses and Aggregations, see [Search extensibility](https://doc.ibexa.co/en/saas/search/extensibility/create_custom_search_criterion/index.md). # Legacy search engine > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). Legacy search engine overview. Legacy search engine is the default search engine. It's SQL-based and uses Doctrine's database connection. The connections are defined in the same way as for storage engine, and no further specific configuration is needed. Legacy search engine is recommended for basic needs and isn't intended in production. It allows you to use filtering and full-text search, but with some limitations. For more information, check [search engine comparison](https://doc.ibexa.co/en/saas/search/search_engines/search_engines/#search-engines-comparison) > **Tip: Tip** > > The features and performance of Legacy search engine are limited. If you have specific search or performance needs, it's recommended to use [Solr](https://doc.ibexa.co/en/saas/search/search_engines/solr_search_engine/solr_overview/index.md) or [Elasticsearch](https://doc.ibexa.co/en/saas/search/search_engines/elasticsearch/elasticsearch_overview/index.md) instead. > > Using the Legacy search engine disables most shop features, such as product search. # Configure repository with Legacy search engine > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). Configure Legacy search engine to use it with Cohesivo. Search can be configured independently from storage, and the following configuration example shows both the default values, and how you configure legacy as the search engine: ```yaml ibexa: repositories: main: storage: engine: legacy connection: default search: engine: legacy connection: default ``` # Search API > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). You can search for content, locations and products by using the PHP API. Fine-tune the search with Search Criteria, Sort Clauses and Aggregations. You can search for content with the PHP API in two ways. To do this, you can use the [`SearchService`](#searchservice) or [Repository filtering](#repository-filtering). ## SearchService [`SearchService`](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-Core-Repository-SearchService.html) enables you to perform search queries by using the PHP API. The service should be [injected into the constructor of your command or controller](https://doc.ibexa.co/en/saas/api/php_api/php_api/#service-container). > **Tip: SearchService in the back office** > > `SearchService` is also used in the back office of Cohesivo, in components such as Universal Discovery Widget or Sub-items List. ### Perform search To search through content you need to create a [`LocationQuery`](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-Core-Repository-Values-Content-LocationQuery.html) and provide your Search Criteria as a series of Criterion objects. For example, to search for all content of a selected content type, use one Criterion, [`Criterion\ContentTypeIdentifier`](https://doc.ibexa.co/en/saas/search/criteria_reference/contenttypeidentifier_criterion/index.md) (line 14). The following command takes the content type identifier as an argument and lists all results: ```php // ... use Ibexa\Contracts\Core\Repository\SearchService; use Ibexa\Contracts\Core\Repository\Values\Content\LocationQuery; use Ibexa\Contracts\Core\Repository\Values\Content\Query\Criterion; // ... class FindContentCommand extends Command { // ... protected function execute(InputInterface $input, OutputInterface $output): int { $contentTypeIdentifier = $input->getArgument('contentTypeIdentifier'); $query = new LocationQuery(); $query->filter = new Criterion\ContentTypeIdentifier($contentTypeIdentifier); $result = $this->searchService->findContentInfo($query); $output->writeln('Found ' . $result->totalCount . ' items'); foreach ($result->searchHits as $searchHit) { $output->writeln($searchHit->valueObject->name); } return self::SUCCESS; } } ``` [`SearchService::findContentInfo`](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-Core-Repository-SearchService.html#method_findContentInfo) (line 16) retrieves [`ContentInfo`](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-Core-Persistence-Content-ContentInfo.html) objects of the found content items. You can also use [`SearchService::findContent`](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-Core-Repository-SearchService.html#method_findContent) to get full Content objects, together with their field information. To query for a single result, for example by providing a Content ID, use the [`SearchService::findSingle`](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-Core-Repository-SearchService.html#method_findSingle) method: ```php use Ibexa\Contracts\Core\Repository\SearchService; use Ibexa\Contracts\Core\Repository\Values\Content\Query\Criterion; use Symfony\Component\Console\Output\OutputInterface; $contentId = 12345; $criterion = new Criterion\ContentId($contentId); /** @var SearchService $searchService */ $result = $searchService->findSingle($criterion); /** @var OutputInterface $output */ $output->writeln($result->getName() ?? ''); ``` > **Tip: Tip** > > For full list and details of available Search Criteria, see [Search Criteria reference](https://doc.ibexa.co/en/saas/search/criteria_reference/search_criteria_reference/index.md). > **Note: Search result limit** > > By default search returns up to 25 results. You can change it by setting a different limit to the query: > > ```php > use Ibexa\Contracts\Core\Repository\Values\Content\LocationQuery; > > $query = new LocationQuery(); > $query->limit = 100; > ``` #### Disable result count By default, a search query also counts all matching results. If you don't need the total count, set `performCount` to `false` on [`Query`](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-Core-Repository-Values-Content-Query.html) or [`LocationQuery`](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-Core-Repository-Values-Content-LocationQuery.html) to improve performance, especially for large result sets. ```php // For location searches $locationQuery = new LocationQuery(); $locationQuery->performCount = false; // For content searches $contentQuery = new Query(); $contentQuery->performCount = false; ``` When `performCount` is set to `false`, `$result->totalCount` is `null`. #### Search with `query` and `filter` You can use two properties of the `Query` object to search for content: `query` and `filter`. In contrast to `filter`, `query` has an effect of search scoring (relevancy). It affects default sorting if no Sort Clause is used. As such, `query` is recommended when the search is based on user input. The difference between `query` and `filter` is only relevant when using Solr or Elasticsearch search engine. With the Legacy search engine both properties give identical results. #### Process large result sets To process a large result set, use [`Ibexa\Contracts\Core\Repository\Iterator\BatchIterator`](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-Core-Repository-Iterator-BatchIterator.html). `BatchIterator` divides the results of search or filtering into smaller batches. This enables iterating over results that are too large to handle due to memory constraints. `BatchIterator` takes one of the available adapters ([`\Ibexa\Contracts\Core\Repository\Iterator\BatchIteratorAdapter`](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/namespaces/ibexa-contracts-core-repository-iterator-batchiteratoradapter.html)) and optional batch size. For example: ```php use Ibexa\Contracts\Core\Repository\Iterator\BatchIterator; use Ibexa\Contracts\Core\Repository\Iterator\BatchIteratorAdapter; use Ibexa\Contracts\Core\Repository\SearchService; use Ibexa\Contracts\Core\Repository\Values\Content\LocationQuery; use Symfony\Component\Console\Output\OutputInterface; $query = new LocationQuery(); /** @var SearchService $searchService */ $iterator = new BatchIterator(new BatchIteratorAdapter\LocationSearchAdapter($searchService, $query)); foreach ($iterator as $result) { /** @var OutputInterface $output */ $output->writeln($result->valueObject->getContentInfo()->name); } ``` You can also define the batch size by setting `$iterator->setBatchSize()`. The following BatchIterator adapters are available, for both `query` and `filter` searches, here with the method they replace: | Adapter | Regular method | | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | [`ContentFilteringAdapter`](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-Core-Repository-Iterator-BatchIteratorAdapter-ContentFilteringAdapter.html) | [`ContentService::find()`](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-Core-Repository-ContentService.html#method_find) | | [`ContentInfoSearchAdapter`](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-Core-Repository-Iterator-BatchIteratorAdapter-ContentInfoSearchAdapter.html) | [`SearchService::findContentInfo()`](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-Core-Repository-SearchService.html#method_findContentInfo) | | [`ContentSearchAdapter`](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-Core-Repository-Iterator-BatchIteratorAdapter-ContentSearchAdapter.html) | [`SearchService::findContent()`](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-Core-Repository-SearchService.html#method_findContent) | | [`RelationListIteratorAdapter`](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-Core-Repository-Iterator-BatchIteratorAdapter-RelationListIteratorAdapter.html) | [`ContentService::loadRelationList()`](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-Core-Repository-ContentService.html#method_loadRelationList) | | [`LocationFilteringAdapter`](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-Core-Repository-Iterator-BatchIteratorAdapter-LocationFilteringAdapter.html) | [`LocationService::find()`](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-Core-Repository-LocationService.html#method_find) | | [`LocationSearchAdapter`](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-Core-Repository-Iterator-BatchIteratorAdapter-LocationSearchAdapter.html) | [`SearchService::findLocations()`](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-Core-Repository-SearchService.html#method_findLocations) | | [`AttributeDefinitionFetchAdapter`](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-ProductCatalog-Iterator-BatchIteratorAdapter-AttributeDefinitionFetchAdapter.html) | [`AttributeDefinitionServiceInterface::findAttributesDefinitions()`](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-ProductCatalog-AttributeDefinitionServiceInterface.html#method_findAttributesDefinitions) | | [`AttributeGroupFetchAdapter`](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-ProductCatalog-Iterator-BatchIteratorAdapter-AttributeGroupFetchAdapter.html) | [`AttributeGroupServiceInterface::findAttributeGroups()`](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-ProductCatalog-AttributeGroupServiceInterface.html#method_findAttributeGroups) | | [`CurrencyFetchAdapter`](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-ProductCatalog-Iterator-BatchIteratorAdapter-CurrencyFetchAdapter.html) | [`CurrencyServiceInterface::findCurrencies()`](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-ProductCatalog-CurrencyServiceInterface.html#method_findCurrencies) | | [`ProductTypeListAdapter`](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-ProductCatalog-Iterator-BatchIteratorAdapter-ProductTypeListAdapter.html) | [`ProductTypeServiceInterface::findProductTypes()`](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-ProductCatalog-ProductTypeServiceInterface.html#method_findProductTypes) | ## Repository filtering You can use the `ContentService::find(Filter)` method to find content items or `LocationService::find(Filter)` to find locations by using a defined Filter. `ContentService::find` returns an iterable [`ContentList`](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-Core-Repository-Values-Content-ContentList.html) while `LocationService::find` returns an iterable [`LocationList`](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-Core-Repository-Values-Content-LocationList.html). Filtering differs from search. It doesn't use the `SearchService` and isn't based on indexed data. [`Filter`](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-Core-Repository-Values-Filter-Filter.html) enables you to configure a query by using chained methods to select criteria, sorting, limit, and offset. For example, the following command lists all content items under the specified parent location and sorts them by name in descending order: ```php // ... use Ibexa\Contracts\Core\Repository\ContentService; use Ibexa\Contracts\Core\Repository\Values\Content\Query; use Ibexa\Contracts\Core\Repository\Values\Content\Query\Criterion; use Ibexa\Contracts\Core\Repository\Values\Content\Query\SortClause; use Ibexa\Contracts\Core\Repository\Values\Filter\Filter; class FilterCommand extends Command { // ... protected function execute(InputInterface $input, OutputInterface $output): int { $parentLocationId = (int)$input->getArgument('parentLocationId'); $filter = new Filter(); $filter ->withCriterion(new Criterion\ParentLocationId($parentLocationId)) ->withSortClause(new SortClause\ContentName(Query::SORT_DESC)); $result = $this->contentService->find($filter, []); $output->writeln('Found ' . $result->getTotalCount() . ' items'); foreach ($result as $content) { $output->writeln($content->getName() ?? 'No content name'); } return self::SUCCESS; } } ``` The same Filter can be applied to find locations instead of content items, for example: ```php // ... use Ibexa\Contracts\Core\Repository\LocationService; use Ibexa\Contracts\Core\Repository\Values\Content\Query; use Ibexa\Contracts\Core\Repository\Values\Content\Query\Criterion; use Ibexa\Contracts\Core\Repository\Values\Content\Query\SortClause; use Ibexa\Contracts\Core\Repository\Values\Filter\Filter; class FilterCommand extends Command { // ... protected function execute(InputInterface $input, OutputInterface $output): int { $parentLocationId = (int)$input->getArgument('parentLocationId'); $filter = new Filter(); $filter ->withCriterion(new Criterion\ParentLocationId($parentLocationId)) ->withSortClause(new SortClause\ContentName(Query::SORT_DESC)); $result = $this->locationService->find($filter, []); $output->writeln('Found ' . $result->getTotalCount() . ' items'); foreach ($result as $content) { $output->writeln($content->getContent()->getName()); } return self::SUCCESS; } } ``` > **Caution: Caution** > > The total count is the total number of matched items, regardless of pagination settings. > **Tip: Repository filtering is SiteAccess-aware** > > Repository filtering is SiteAccess-aware, which means you can skip the second argument of the `find` methods. In that case languages from a current context are injected and added as a LanguageCode Criterion filter. You can use the following methods of the Filter: - `withCriterion` - add the first Criterion to the Filter - `andWithCriterion` - add another Criterion to the Filter using a LogicalAnd operation. If this is the first Criterion, this method works like `withCriterion` - `orWithCriterion` - add a Criterion using a LogicalOr operation. If this is the first Criterion, this method works like `withCriterion` - `withSortClause` - add a Sort Clause to the Filter - `sliceBy` - set limit and offset for pagination - `reset` - remove all Criteria, Sort Clauses, and pagination settings The following example filters for Folder content items under the parent location 2, sorts them by publication date and returns 10 results, starting from the third one: ```php use Ibexa\Contracts\Core\Repository\Values\Content\Query; use Ibexa\Contracts\Core\Repository\Values\Content\Query\Criterion; use Ibexa\Contracts\Core\Repository\Values\Content\Query\SortClause; use Ibexa\Contracts\Core\Repository\Values\Filter\Filter; $filter = new Filter(); $filter ->withCriterion(new Criterion\ContentTypeIdentifier('folder')) ->andWithCriterion(new Criterion\ParentLocationId(2)) ->withSortClause(new SortClause\DatePublished(Query::SORT_ASC)) ->sliceBy(10, 2); ``` > **Note: Search Criteria and Sort Clause availability** > > Not all Search Criteria and Sort Clauses are available for use in repository filtering. > > Only Criteria implementing [`FilteringCriterion`](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-Core-Repository-Values-Filter-FilteringCriterion.html) and Sort Clauses implementing [`FilteringSortClause`](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-Core-Repository-Values-Filter-FilteringSortClause.html) are supported. > > See [Search Criteria](https://doc.ibexa.co/en/saas/search/criteria_reference/search_criteria_reference/index.md) and [Sort Clause reference](https://doc.ibexa.co/en/saas/search/sort_clause_reference/sort_clause_reference/index.md) for details. > **Tip: Tip** > > It's recommended to use an IDE that can recognize type hints when working with Repository Filtering. If you try to use an unsupported Criterion or Sort Clause, the IDE indicates an issue. ## Search in controller You can use the `SearchService` or repository filtering in a controller, as long as you provide the required parameters. For example, in the code below, `locationId` is provided to list all children of a location by using the `SearchService`. ```php // ... use Ibexa\Bundle\Core\Controller; use Ibexa\Contracts\Core\Repository\SearchService; use Ibexa\Contracts\Core\Repository\Values\Content\LocationQuery; use Ibexa\Contracts\Core\Repository\Values\Content\Query\Criterion; use Symfony\Component\HttpFoundation\Response; class CustomController extends Controller { // ... public function showContentAction(int $locationId): Response { $query = new LocationQuery(); $query->filter = new Criterion\ParentLocationId($locationId); $results = $this->searchService->findContentInfo($query); $items = []; foreach ($results->searchHits as $searchHit) { $items[] = $searchHit; } return $this->render('@ibexadesign/full/custom.html.twig', [ 'items' => $items, ]); } } ``` The rendering of results is then relegated to [templates](https://doc.ibexa.co/en/saas/templating/templates/templates/index.md) (lines 22-24). When using Repository filtering, provide the results of `ContentService::find()` as parameters to the view: ```php // ... use Ibexa\Bundle\Core\Controller; use Ibexa\Contracts\Core\Repository\ContentService; use Ibexa\Contracts\Core\Repository\Values\Content\Query\Criterion\ParentLocationId; use Ibexa\Contracts\Core\Repository\Values\Filter\Filter; use Ibexa\Core\MVC\Symfony\View\ContentView; class CustomFilterController extends Controller { // ... public function showChildrenAction(ContentView $view): ContentView { $filter = new Filter(); $filter ->withCriterion(new ParentLocationId($view->getLocation()->id)); $view->setParameters( [ 'items' => $this->contentService->find($filter), ] ); return $view; } } ``` ### Paginate search results To paginate search or filtering results, it's recommended to use the [Pagerfanta library](https://github.com/BabDev/Pagerfanta) and [Cohesivo's adapters for it.](https://github.com/ibexa/core/blob/6.0/src/lib/Pagination/Pagerfanta/Pagerfanta.php) ```php // ... use Ibexa\Core\Pagination\Pagerfanta\ContentSearchAdapter; use Pagerfanta\Pagerfanta; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; class PaginationController extends Controller { // ... public function showContentAction(Request $request, int $locationId): Response { $query = new LocationQuery(); $query->filter = new Criterion\ParentLocationId($locationId); $pager = new Pagerfanta( new ContentSearchAdapter($query, $this->searchService) ); $pager->setMaxPerPage(3); $pager->setCurrentPage($request->get('page', 1)); return $this->render( '@ibexadesign/full/custom_pagination.html.twig', [ 'totalItemCount' => $pager->getNbResults(), 'pagerItems' => $pager, ] ); } } ``` Pagination can then be rendered for example using the following template: ```html+twig {% for item in pagerItems %}

    {{ ibexa_content_name(item) }}

    {% endfor %} {% if pagerItems.haveToPaginate() %} {{ pagerfanta(pagerItems, 'ibexa') }} {% endif %} ``` For more information and examples, see [PagerFanta documentation](https://www.babdev.com/open-source/packages/pagerfanta/docs/3.x/usage). #### Pagerfanta adapters | Adapter class name | Description | | ------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | [`ContentSearchAdapter`](https://github.com/ibexa/core/blob/6.0/src/lib/Pagination/Pagerfanta/ContentSearchAdapter.php) | Makes a search against passed Query and returns [`Content`](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-Core-Repository-Values-Content-Content.html) objects. | | [`ContentSearchHitAdapter`](https://github.com/ibexa/core/blob/6.0/src/lib/Pagination/Pagerfanta/ContentSearchHitAdapter.php) | Makes a search against passed Query and returns [`SearchHit`](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-Core-Repository-Values-Content-Search-SearchHit.html) objects instead. | | [`LocationSearchAdapter`](https://github.com/ibexa/core/blob/6.0/src/lib/Pagination/Pagerfanta/LocationSearchAdapter.php) | Makes a location search against passed Query and returns [`Location`](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-Core-Repository-Values-Content-Location.html) objects. | | [`LocationSearchHitAdapter`](https://github.com/ibexa/core/blob/6.0/src/lib/Pagination/Pagerfanta/LocationSearchHitAdapter.php) | Makes a location search against passed Query and returns [`SearchHit`](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-Core-Repository-Values-Content-Search-SearchHit.html) objects instead. | | [`ContentFilteringAdapter`](https://github.com/ibexa/core/blob/6.0/src/lib/Pagination/Pagerfanta/ContentFilteringAdapter.php) | Applies a Content filter and returns a [`ContentList`](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-Core-Repository-Values-Content-ContentList.html) object. | | [`LocationFilteringAdapter`](https://github.com/ibexa/core/blob/6.0/src/lib/Pagination/Pagerfanta/LocationFilteringAdapter.php) | Applies a location filter and returns a [`LocationList`](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-Core-Repository-Values-Content-LocationList.html) object. | | `AttributeDefinitionListAdapter` | Makes a search for product attributes and returns an [`AttributeDefinitionListInterface`](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-ProductCatalog-Values-AttributeDefinition-AttributeDefinitionListInterface.html) object. | | `AttributeGroupListAdapter` | Makes a search for product attribute groups and returns an [`AttributeGroupListInterface`](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-ProductCatalog-Values-AttributeGroup-AttributeGroupListInterface.html) object. | | `CurrencyListAdapter` | Makes a search for currencies and returns a [`CurrencyListInterface`](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-ProductCatalog-Values-Currency-CurrencyListInterface.html) object. | | `CustomPricesAdapter` | Makes a search for custom prices and returns a `CustomPrice` object. | | `CustomerGroupListAdapter` | Makes a search for customer groups and returns a [`CustomerGroupListInterface`](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-ProductCatalog-Values-CustomerGroup-CustomerGroupListInterface.html) object. | | `ProductListAdapter` | Makes a search for products and returns a [`ProductListInterface`](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-ProductCatalog-Values-Product-ProductListInterface.html) object. | | `ProductTypeListAdapter` | Makes a search for product types and returns a [`ProductTypeListInterface`](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-ProductCatalog-Values-ProductType-ProductTypeListInterface.html) object. | | `RegionListAdapter` | Makes a search for regions and returns a [`RegionListInterface`](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-ProductCatalog-Values-Region-RegionListInterface.html) object. | | `ShoppingListAdapter` | Makes a search for shopping lists and returns a [`ShoppingListCollectionInterface`](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-ShoppingList-Value-ShoppingListCollectionInterface.html) object. | ## Complex search For more complex searches, you need to combine multiple Criteria. You can do it using logical operators: `LogicalAnd`, `LogicalOr`, and `LogicalNot`. ```php $query = new LocationQuery(); $query->query = new Criterion\LogicalAnd([ new Criterion\Subtree($this->locationService->loadLocation($locationId)->pathString), new Criterion\ContentTypeIdentifier($contentTypeIdentifier), ]); $result = $this->searchService->findContentInfo($query); $output->writeln('Found ' . $result->totalCount . ' items'); foreach ($result->searchHits as $searchHit) { $output->writeln($searchHit->valueObject->name); } ``` This example takes three parameters from a command — `$text`, `$contentTypeId`, and `$locationId`. It then combines them using `Criterion\LogicalAnd` to search for content items that belong to a specific subtree, have the chosen content type and contain the provided text (lines 3-6). This also shows that you can get the total number of search results using the `totalCount` property of search results (line 9). You can also nest different operators to construct more complex queries. The example below uses the `LogicalNot` operator to search for all content containing a given phrase that doesn't belong to the provided Section: ```php $query->query = new Criterion\LogicalAnd([ new Criterion\Subtree($this->locationService->loadLocation($locationId)->pathString), new Criterion\ContentTypeIdentifier($contentTypeIdentifier), new Criterion\FullText($text), new Criterion\LogicalNot( new Criterion\SectionIdentifier('Media') ), ]); ``` ### Combine independent Criteria Criteria are independent of one another. This can lead to unexpected behavior, for instance because content can have multiple locations. For example, a content item has two locations: visible location A and hidden location B. You perform the following query: ```php use Ibexa\Contracts\Core\Repository\Values\Content\LocationQuery; use Ibexa\Contracts\Core\Repository\Values\Content\Query\Criterion; use Ibexa\Contracts\Core\Repository\Values\Content\Query\Criterion\LocationId; use Ibexa\Contracts\Core\Repository\Values\Content\Query\Criterion\Visibility; $query = new LocationQuery(); $bLocationId = 12345; $query->filter = new Criterion\LogicalAnd([ new LocationId($bLocationId), new Visibility(Visibility::VISIBLE), ]); ``` The query searches for location B by using the [`LocationId` Criterion](https://doc.ibexa.co/en/saas/search/criteria_reference/locationid_criterion/index.md), and for visible content by using the [`Visibility` Criterion](https://doc.ibexa.co/en/saas/search/criteria_reference/visibility_criterion/index.md). Even though the location B is hidden, the query finds the content because both conditions are satisfied: - the content item has location B - the content item is visible (it has the visible location A) ## Sort results To sort the results of a query, use one of more [Sort Clauses](https://doc.ibexa.co/en/saas/search/sort_clause_reference/sort_clause_reference/index.md). For example, to order search results by their publication date, from oldest to newest, and then alphabetically by content name, add the following Sort Clauses to the query: ```php $query->sortClauses = [ new SortClause\DatePublished(LocationQuery::SORT_ASC), new SortClause\ContentName(LocationQuery::SORT_DESC), ]; ``` > **Tip: Tip** > > For the full list and details of available Sort Clauses, see [Sort Clause reference](https://doc.ibexa.co/en/saas/search/sort_clause_reference/sort_clause_reference/index.md). ## Aggregation > **Caution: Feature support** > > Aggregation is only available in the Solr and Elasticsearch search engines. With aggregations you can find the count of search results or other result information for each Aggregation type. To do this, you use of the query's `$aggregations` property: ```php $contentTypeTermAggregation = new ContentTypeTermAggregation('content_type'); $contentTypeTermAggregation->setLimit(5); $contentTypeTermAggregation->setMinCount(10); $query->aggregations[] = $contentTypeTermAggregation; ``` The name of the aggregation must be unique in the given query. Access the results by using the `get()` method of the aggregation: ```php $contentByType = $results->aggregations->get('content_type'); ``` Aggregation results contain the name of the result and the count of found items: ```php foreach ($contentByType as $contentType => $count) { $output->writeln($contentType->getName() . ': ' . $count); } ``` With field aggregations you can group search results according to the value of a specific field. In this case the aggregation takes the content type identifier and the field identifier as parameters. The following example creates an aggregation named `selection` that groups results according to the value of the `topic` field in the `article` content type: ```php $query->aggregations[] = new SelectionTermAggregation('selection', 'blog_post', 'topic'); ``` With term aggregation you can define additional limits to the results. The following example limits the number of terms returned to 5 and only considers terms that have 10 or more results: ```php $contentTypeTermAggregation = new ContentTypeTermAggregation('content_type'); $contentTypeTermAggregation->setLimit(5); $contentTypeTermAggregation->setMinCount(10); ``` To use a range aggregation, you must provide a `ranges` array containing a set of `Range` objects that define the borders of the specific range sets. ```php use Ibexa\Contracts\Core\Repository\Values\Content\LocationQuery; use Ibexa\Contracts\Core\Repository\Values\Content\Query\Aggregation\Field\IntegerRangeAggregation; use Ibexa\Contracts\Core\Repository\Values\Content\Query\Aggregation\Range; $ranges = [ Range::ofInt(1, 30), Range::ofInt(30, 60), Range::ofInt(60, null), ]; $query = new LocationQuery(); $query->aggregations[] = new IntegerRangeAggregation('range', 'person', 'age', $ranges); ``` > **Note: Note** > > The beginning of the range is included and the end is excluded, so a range between 1 and 30 includes value `1`, but not `30`. > > `null` means that a range doesn't have an end. In the example all values above (and including) 60 are included in the last range. See [Aggregation reference](https://doc.ibexa.co/en/saas/search/aggregation_reference/aggregation_reference/index.md) for details of all available aggregations. ## Search with embeddings > **Note: Feature support** > > Searching with embeddings requires a search engine that supports it, such as Elasticsearch or Solr 9.8.1+. Embeddings are numerical representations that capture the meaning of text, images, or other content. AI providers generate embeddings by converting words or documents into lists of numbers, instead of treating them as plain text. Such lists, aka vectors, can then be compared to find content with similar meaning. Searching with embeddings enables matching content based on meaning rather than exact text matches. Instead of comparing keywords, the system compares vectors that represent the semantic meaning of content and the query input. > **Note: Taxonomy suggestions** > > Embedding queries have been introduced primarily to support the [Taxonomy suggestions](https://doc.ibexa.co/en/saas/content_management/taxonomy/taxonomy/#taxonomy-suggestions) feature, therefore embedding search integration is provided for `TaxonomyEmbedding`. You can narrow down the search results, for example, by content type or location. To do this, combine searching with embeddings with filters. Repository search also respects the permissions of the current user. An embedding query is represented by the [`Ibexa\Contracts\Core\Repository\Values\Content\EmbeddingQuery`](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-Core-Repository-Values-Content-EmbeddingQuery.html) value object. The object encapsulates the embedding used for similarity search and optional search parameters such as filtering, pagination, aggregations, and result counting. ### Use embedding queries in search Embedding queries are executed through the search API in the same way as other search requests. You build an `EmbeddingQuery` instance by using a builder and pass it to the search service. This example shows a minimal embedding query executed directly through the search service: ```php embeddingProviderResolver->resolve(); $embedding = $embeddingProvider->getEmbedding('example_content'); $query = EmbeddingQueryBuilder::create() ->withEmbedding(new TaxonomyEmbedding($embedding)) ->setFilter(new ContentTypeIdentifier('article')) ->setLimit(10) ->setOffset(0) ->setPerformCount(true) ->build(); $result = $this->searchService->findContent($query); $io->success(sprintf('Found %d items.', $result->totalCount)); foreach ($result->searchHits as $searchHit) { assert($searchHit instanceof SearchHit); /** @var \Ibexa\Contracts\Core\Repository\Values\Content\Content $content */ $content = $searchHit->valueObject; $contentInfo = $content->versionInfo->contentInfo; $io->writeln(sprintf( '%d: %s', $contentInfo->id, $contentInfo->name )); } return self::SUCCESS; } } ``` For more information, see [Embeddings reference](https://doc.ibexa.co/en/saas/search/embeddings_reference/embeddings_reference/index.md). ## Search in trash In the user interface, on the **Trash** screen, you can search for content items, and then sort the results based on different criteria. To search the trash with the API, use the `TrashService::findInTrash` method to submit a query for content items that are held in trash. Searching in trash supports a limited set of Criteria and Sort Clauses. For a list of supported Criteria and Sort Clauses, see [Search in trash reference](https://doc.ibexa.co/en/saas/search/search_in_trash_reference/index.md). > **Note: Note** > > Searching through the trashed content items operates directly on the database, therefore you cannot use external search engines, such as Solr or Elasticsearch, and it's impossible to reindex the data. ```php use Ibexa\Contracts\Core\Repository\TrashService; use Ibexa\Contracts\Core\Repository\Values\Content\Query; //... $query = new Query(); $query->filter = new Query\Criterion\ContentTypeId($contentTypeId); $results = $this->trashService->findTrashItems($query); foreach ($results->items as $trashedLocation) { $output->writeln($trashedLocation->getContentInfo()->name); } ``` > **Caution: Caution** > > Make sure that you set the Criterion on the `filter` property. It's impossible to use the `query` property, because the search in trash operation filters the database instead of querying. # Search Criteria and Sort Clauses > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). Search Criteria and Sort Clauses help you fine-tune searches done by using the Search API. Search Criteria and Sort Clauses are value object classes used for building a search query, to define filter criteria and ordering of the result set. Cohesivo provides a number of standard Search Criteria and Sort Clauses that you can use out of the box and that should cover the majority of use cases. For an example of how to use and combine Criteria and Sort Clauses, refer to [Searching in PHP API](https://doc.ibexa.co/en/saas/search/search_api/index.md). ## Search engine handling of Search Criteria and Sort Clauses As Search Criteria and Sort Clauses are value objects which are used to define the query from API perspective, they're common for all storage engines. Each storage engine needs to implement its own handlers for the corresponding Criterion and Sort Clause value object, which are used to translate the value object into a storage-specific search query. As an example take a look at the [`ContentId` Criterion handler](https://github.com/ibexa/core/blob/6.0/src/lib/Search/Legacy/Content/Common/Gateway/CriterionHandler/ContentId.php) in Legacy search engine or [`ContentId` Criterion handler](https://github.com/ibexa/solr/blob/6.0/src/lib/Query/Common/CriterionVisitor/ContentIdIn.php) in Solr search engine. ## Custom Criteria and Sort Clauses Sometimes you may find that standard Search Criteria and Sort Clauses provided with Cohesivo aren't sufficient for your needs. Most often this is the case if you have a custom field type using external storage which cannot be searched using the standard field Criterion. > **Note: Note** > > Legacy (SQL-based) search can also be used in `ibexa_keyword` external storage. In such cases you can implement a custom Criterion or Sort Clause, together with the corresponding handlers for the storage engine you're using. > **Caution: Using Field Criterion or Sort Clause with large databases** > > Field Criterion and Sort Clause don't perform well by design when using SQL database. If you have a large database and want to use them, you either need to use the Solr search engine, or develop your own Custom Criterion or Sort Clause. This way you can avoid using the attributes (fields) database table, and instead use a custom simplified table which can handle the amount of data you have. ### Difference between Content and Location Search There are two basic types of searches, you can either search for locations or for content. Each type has dedicated methods in the Search Service: | Type of search | Method in Search Service | | -------------- | ------------------------ | | Content | `findContent()` | | Content | `findContentInfo()` | | Content | `findSingle()` | | Location | `findLocations()` | All Criteria and Sort Clauses are accepted with Location Search, but not all of them can be used with Content Search. The reason for this is that while one location always has exactly one content item, one content item can have multiple locations. In this context some Criteria and Sort Clauses would produce ambiguous queries that would not be accepted by Content Search. Content Search explicitly refuses to accept Criteria and Sort Clauses implementing these abstract classes: - `Ibexa\Contracts\Core\Repository\Values\Content\Query\Criterion\Location` - `Ibexa\Contracts\Core\Repository\Values\Content\SortClause\Criterion\Location` ### Configuring custom Criterion and Sort Clause handlers After you have implemented your Criterion / Sort Clause and its handler, you need to configure the handler for the [service container](https://doc.ibexa.co/en/saas/api/php_api/php_api/#service-container) by using dedicated service tags for each type of search. Doing so automatically registers it and handle your Criterion / Search Clause when it's given as a parameter to one of the Search Service methods. Available tags for Criterion handlers in Legacy Storage Engine are: - `ibexa.search.legacy.gateway.criterion_handler.content` - `ibexa.search.legacy.gateway.criterion_handler.location` Available tags for Sort Clause handlers in Legacy Storage Engine are: - `ibexa.search.legacy.gateway.sort_clause_handler.content` - `ibexa.search.legacy.gateway.sort_clause_handler.location` > **Note: Note** > > You can find all the native handlers and the tags for the Legacy Storage Engine in files located in `core/src/lib/Resources/settings/storage_engines/`. > **Tip: Tip** > > When you search in trash, use the following service tags: > > - for Criterion handlers: `ibexa.core.trash.search.legacy.gateway.criterion_handler` > - for Sort Clause handlers: `ibexa.core.trash.search.legacy.gateway.sort_clause_handler` > > For more information about searching for content items in Trash, see [Search in trash](https://doc.ibexa.co/en/saas/search/search_api/#search-in-trash). > > For more information about the Criteria and Sort Clauses that are supported when searching for trashed content items, see [Searching in trash reference](https://doc.ibexa.co/en/saas/search/search_in_trash_reference/index.md). The following example shows how to register a ContentId Criterion handler, common for both Content and Location Search: ```yaml services: Ibexa\Core\Search\Legacy\Content\Common\Gateway\CriterionHandler\ContentId: arguments: ['@ibexa.api.storage_engine.legacy.dbhandler'] tags: - {name: ibexa.search.legacy.gateway.criterion_handler.content} - {name: ibexa.search.legacy.gateway.criterion_handler.location} ``` The following example shows how to register a Depth Sort Clause handler for Location Search: ```yaml Ibexa\Core\Search\Legacy\Content\Location\Gateway\SortClauseHandler\Location\Depth: arguments: ['@ibexa.api.storage_engine.legacy.dbhandler'] tags: - {name: ibexa.search.legacy.gateway.sort_clause_handler.location} ``` For more information about passing parameters, see [Symfony Service Container documentation](https://symfony.com/doc/7.4/service_container.html#service-container-parameters). ## Search using custom Field Criterion [REST] REST search can be performed by calling the `POST /views` method with a custom `FieldCriterion`. This allows you to build custom content logic queries with nested logical operators OR/AND/NOT. Custom Field Criterion search mirrors the one already existing in PHP API `Ibexa\Contracts\Core\Repository\Values\Content\Query\Criterion\Field` by exposing it to REST. ### Example of custom Content Query ```json "ContentQuery":{ "Query":{ "OR":[ { "AND":[ { "Field":{ "name":"name", "operator":"CONTAINS", "value":"foo" } }, { "Field":{ "name":"info", "operator":"CONTAINS", "value":"bar" } } ] }, { "AND":[ { "Field":{ "name":"name", "operator":"CONTAINS", "value":"barfoo" } }, { "Field":{ "name":"info", "operator":"CONTAINS", "value":"baz" } } ] } ] } } ``` # Search Criteria reference > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). Search Criteria help define and fine-tune search queries for content and locations. Search Criteria are filters for content and location Search and [Repository filtering](https://doc.ibexa.co/en/saas/search/search_api/#repository-filtering). Criteria can take some of the following arguments: - `target` - when the Criterion supports targeting a specific field, example: `FieldDefinition` or Metadata identifier - `value` - the value(s) to filter on, typically a scalar or array of scalars - `operator` - constants on `Ibexa\Contracts\Core\Repository\Values\Content\Query\Criterion\Operator`: `IN`, `EQ`, `GT`, `GTE`, `LT`, `LTE`, `LIKE`, `BETWEEN`, `CONTAINS`. Most Criteria don't expose this and select `EQ` or `IN` depending on whether the value is scalar or an array. `IN` and `BETWEEN` always act on an array of values, while the other operators act on single scalar value - `valueData` - additional value data, required by some Criteria, for instance `MapLocationDistance` Support and capabilities of individual Criteria can depend on the search engine. In the Legacy search engine, the field index/sort key column is limited to 255 characters by design. Due to this storage limitation, searching content using the Country field type or Keyword when there are multiple values selected may not return all the expected results. ## Search Criteria | Search Criterion | Search based on | Content Search | Location Search | Filtering | Trash | | -------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------- | -------------- | --------------- | --------- | ----- | | [Ancestor](https://doc.ibexa.co/en/saas/search/criteria_reference/ancestor_criterion/index.md) | Whether the content item is an ancestor of the provided location | Yes | Yes | Yes | | | [ContentId](https://doc.ibexa.co/en/saas/search/criteria_reference/contentid_criterion/index.md) | Content item's ID | Yes | Yes | Yes | | | [ContentName](https://doc.ibexa.co/en/saas/search/criteria_reference/contentname_criterion/index.md) | Content item's name | Yes | Yes | Yes | Yes | | [ContentTypeGroupId](https://doc.ibexa.co/en/saas/search/criteria_reference/contenttypegroupid_criterion/index.md) | ID of the content item's content type group | Yes | Yes | Yes | | | [ContentTypeId](https://doc.ibexa.co/en/saas/search/criteria_reference/contenttypeid_criterion/index.md) | ID of the content item's content type | Yes | Yes | Yes | Yes | | [ContentTypeIdentifier](https://doc.ibexa.co/en/saas/search/criteria_reference/contenttypeidentifier_criterion/index.md) | Identifier of the content item's content type | Yes | Yes | Yes | | | [CurrencyCodeCriterion](https://doc.ibexa.co/en/saas/search/criteria_reference/currencycode_criterion/index.md) | Currency code | Yes | Yes | Yes | | | [CustomField](https://doc.ibexa.co/en/saas/search/criteria_reference/customfield_criterion/index.md) | Custom field | Yes | Yes | | | | [DateMetadata](https://doc.ibexa.co/en/saas/search/criteria_reference/datemetadata_criterion/index.md) | The date when content was created or last modified | Yes | Yes | Yes | Yes | | [Depth](https://doc.ibexa.co/en/saas/search/criteria_reference/depth_criterion/index.md) | Location depth in the content tree | | Yes | Yes | | | [Field](https://doc.ibexa.co/en/saas/search/criteria_reference/field_criterion/index.md) | Content of one of content item's fields | Yes | Yes | | | | [FieldRelation](https://doc.ibexa.co/en/saas/search/criteria_reference/fieldrelation_criterion/index.md) | Content items the content in question has Relations to | Yes | Yes | | | | [FullText](https://doc.ibexa.co/en/saas/search/criteria_reference/fulltext_criterion/index.md) | Full text content of a content item's fields | Yes | Yes | | | | [Image](https://doc.ibexa.co/en/saas/search/criteria_reference/image_criterion/index.md) | Image by specified image attributes | Yes | Yes | | | | [ImageDimensions](https://doc.ibexa.co/en/saas/search/criteria_reference/imagedimensions_criterion/index.md) | Image dimensions: height and width | Yes | Yes | | | | [ImageFileSize](https://doc.ibexa.co/en/saas/search/criteria_reference/imagefilesize_criterion/index.md) | Image size in MB | Yes | Yes | | | | [ImageHeight](https://doc.ibexa.co/en/saas/search/criteria_reference/imageheight_criterion/index.md) | Image height in pixels | Yes | Yes | | | | [ImageMimeType](https://doc.ibexa.co/en/saas/search/criteria_reference/imagemimetype_criterion/index.md) | Image type | Yes | Yes | | | | [ImageOrientation](https://doc.ibexa.co/en/saas/search/criteria_reference/imageorientation_criterion/index.md) | Image orientation | Yes | Yes | | | | [ImageWidth](https://doc.ibexa.co/en/saas/search/criteria_reference/imagewidth_criterion/index.md) | Image width in pixels | Yes | Yes | | | | [IsBookmarked](https://doc.ibexa.co/en/saas/search/criteria_reference/isbookmarked_criterion/index.md) | Whether a location is bookmarked or not | | Yes | Yes | | | [IsContainer](https://doc.ibexa.co/en/saas/search/criteria_reference/iscontainer_criterion/index.md) | Whether a content item is a container (can contain other content items) | Yes | Yes | Yes | | | [IsCurrencyEnabledCriterion](https://doc.ibexa.co/en/saas/search/criteria_reference/iscurrencyenabled_criterion/index.md) | Whether a specified currency is enabled in the system | | | | | | [IsFieldEmpty](https://doc.ibexa.co/en/saas/search/criteria_reference/isfieldempty_criterion/index.md) | Whether a specified field of a content item is empty or not | Yes | Yes | | | | [IsMainLocation](https://doc.ibexa.co/en/saas/search/criteria_reference/ismainlocation_criterion/index.md) | Whether a location is the main location of a content item | | Yes | Yes | | | [IsProductBased](https://doc.ibexa.co/en/saas/search/criteria_reference/isproductbased_criterion/index.md) | Whether content represents a product | Yes | Yes | Yes | | | [IsUserBased](https://doc.ibexa.co/en/saas/search/criteria_reference/isuserbased_criterion/index.md) | Whether content represents a User account | Yes | Yes | Yes | | | [IsUserEnabled](https://doc.ibexa.co/en/saas/search/criteria_reference/isuserenabled_criterion/index.md) | Whether a User account is enabled | Yes | Yes | Yes | | | [LanguageCode](https://doc.ibexa.co/en/saas/search/criteria_reference/languagecode_criterion/index.md) | Whether a content item is translated into the selected language | Yes | Yes | Yes | | | [LocationId](https://doc.ibexa.co/en/saas/search/criteria_reference/locationid_criterion/index.md) | Location ID | Yes | Yes | Yes | | | [LocationRemoteId](https://doc.ibexa.co/en/saas/search/criteria_reference/locationremoteid_criterion/index.md) | Location remote ID | Yes | Yes | Yes | | | [MapLocationDistance](https://doc.ibexa.co/en/saas/search/criteria_reference/maplocationdistance_criterion/index.md) | Distance between the location contained in a MapLocation field and the provided coordinates | Yes | Yes | | | | [MatchAll](https://doc.ibexa.co/en/saas/search/criteria_reference/matchall_criterion/index.md) | Returns all search results | Yes | Yes | Yes | Yes | | [MatchNone](https://doc.ibexa.co/en/saas/search/criteria_reference/matchnone_criterion/index.md) | Returns no search results | Yes | Yes | Yes | Yes | | [ObjectStateId](https://doc.ibexa.co/en/saas/search/criteria_reference/objectstateid_criterion/index.md) | Object state ID | Yes | Yes | Yes | | | [ObjectStateIdentifier](https://doc.ibexa.co/en/saas/search/criteria_reference/objectstateidentifier_criterion/index.md) | Object state Identifier | Yes | Yes | Yes | | | [ParentLocationId](https://doc.ibexa.co/en/saas/search/criteria_reference/parentlocationid_criterion/index.md) | Location ID of a content item's parent | Yes | Yes | Yes | | | [ParentLocationRemoteId](https://doc.ibexa.co/en/saas/search/criteria_reference/parentlocationremoteId_criterion/index.md) | Location remote ID of a content item's parent | Yes | Yes | | | | [Priority](https://doc.ibexa.co/en/saas/search/criteria_reference/priority_criterion/index.md) | Location priority | | Yes | Yes | | | [RemoteId](https://doc.ibexa.co/en/saas/search/criteria_reference/remoteid_criterion/index.md) | Remote content ID | Yes | Yes | Yes | | | [SectionId](https://doc.ibexa.co/en/saas/search/criteria_reference/sectionid_criterion/index.md) | ID of the Section content is assigned to | Yes | Yes | Yes | Yes | | [SectionIdentifier](https://doc.ibexa.co/en/saas/search/criteria_reference/sectionidentifier_criterion/index.md) | Identifier of the Section content is assigned to | Yes | Yes | Yes | | | [Sibling](https://doc.ibexa.co/en/saas/search/criteria_reference/sibling_criterion/index.md) | Locations that are children of the same parent | Yes | Yes | Yes | | | [Subtree](https://doc.ibexa.co/en/saas/search/criteria_reference/subtree_criterion/index.md) | Location subtree | Yes | Yes | Yes | | | [TaxonomyEntryId](https://doc.ibexa.co/en/saas/search/criteria_reference/taxonomy_entry_id/index.md) | Content tagged with Entry ID | Yes | Yes | Yes | | | [TaxonomyNoEntries](https://doc.ibexa.co/en/saas/search/criteria_reference/taxonomy_no_entries/index.md) | Content with no entries assigned from a given taxonomy | Yes | Yes | Yes | | | [TaxonomySubtree](https://doc.ibexa.co/en/saas/search/criteria_reference/taxonomy_subtree/index.md) | Content assigned to a taxonomy entry or any of its descendants | Yes | Yes | | | | [UserEmail](https://doc.ibexa.co/en/saas/search/criteria_reference/useremail_criterion/index.md) | Email address of a User account | Yes | Yes | Yes | | | [UserId](https://doc.ibexa.co/en/saas/search/criteria_reference/userid_criterion/index.md) | User ID | Yes | Yes | Yes | | | [UserLogin](https://doc.ibexa.co/en/saas/search/criteria_reference/userlogin_criterion/index.md) | User login | Yes | Yes | Yes | | | [UserMetadata](https://doc.ibexa.co/en/saas/search/criteria_reference/usermetadata_criterion/index.md) | The creator or modifier of a content item | Yes | Yes | Yes | Yes | | [Visibility](https://doc.ibexa.co/en/saas/search/criteria_reference/visibility_criterion/index.md) | Whether the content item is visible or not | Yes | Yes | Yes | | ### Logical operators All Logical operators are supported by Content and Location Search and [Repository filtering](https://doc.ibexa.co/en/saas/search/search_api/#repository-filtering). | Search Criterion | Search based on | | -------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------- | | [LogicalAnd](https://doc.ibexa.co/en/saas/search/criteria_reference/logicaland_criterion/index.md) | Implements a logical AND Criterion. It matches if ALL of the provided Criteria match. | | [LogicalNot](https://doc.ibexa.co/en/saas/search/criteria_reference/logicalnot_criterion/index.md) | Implements a logical NOT Criterion. It matches if the provided Criterion doesn't match. | | [LogicalOr](https://doc.ibexa.co/en/saas/search/criteria_reference/logicalor_criterion/index.md) | Implements a logical OR Criterion. It matches if at least one of the provided Criteria matches. | # Ancestor Criterion > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). Ancestor Search Criterion The [`Ancestor` Search Criterion](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-Core-Repository-Values-Content-Query-Criterion-Ancestor.html) searches for content that is an ancestor of the provided location, including this location. ## Arguments - `value` - array of location pathStrings ## Example ### PHP ```php use Ibexa\Contracts\Core\Repository\Values\Content\Query; use Ibexa\Contracts\Core\Repository\Values\Content\Query\Criterion; $query = new Query(); /** @var \Ibexa\Contracts\Core\Repository\LocationService $locationService */ $query->query = new Criterion\Ancestor([$locationService->loadLocation(62)->pathString]); ``` ### REST API **XML** ```xml /81/82/ ``` **JSON** ```json "Query": { "Filter": { "AncestorCriterion": "/81/82/" } } ``` ## Use case You can use the Ancestor Search Criterion to create a list of breadcrumbs leading to the Location: ```php use Ibexa\Contracts\Core\Repository\Values\Content\LocationQuery; use Ibexa\Contracts\Core\Repository\Values\Content\Query\Criterion; $locationId = 12345; $query = new LocationQuery(); /** @var \Ibexa\Contracts\Core\Repository\LocationService $locationService */ $query->query = new Criterion\Ancestor([$locationService->loadLocation($locationId)->pathString]); /** @var \Ibexa\Contracts\Core\Repository\SearchService $searchService */ $results = $searchService->findLocations($query); $breadcrumbs = []; foreach ($results->searchHits as $searchHit) { $breadcrumbs[] = $searchHit; } return $this->render('parts/breadcrumbs.html.twig', [ 'breadcrumbs' => $breadcrumbs, ]); ``` ```html+twig {% for breadcrumb in breadcrumbs %} {% if not loop.first %} -> {% endif %} {% if not loop.last %} {{ breadcrumb.valueObject.contentInfo.name }} {% else %} {{ breadcrumb.valueObject.contentInfo.name }} {% endif %} {% endfor %} ``` # ContentId Criterion > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). ContentId Search Criterion The [`ContentId` Search Criterion](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-Core-Repository-Values-Content-Query-Criterion-ContentId.html) searches for content by its ID. ## Arguments - `value` - int(s) representing the Content ID(s) ## Example ### PHP ```php use Ibexa\Contracts\Core\Repository\Values\Content\Query; use Ibexa\Contracts\Core\Repository\Values\Content\Query\Criterion; $query = new Query(); $query->query = new Criterion\ContentId([62, 64]); ``` ### REST API **XML** ```xml 1,52 ``` **JSON** ```json "Query": { "Filter": { "ContentIdCriterion": "1,52" } } ``` # ContentName Criterion > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). ContentName Search Criterion The [`ContentName` Search Criterion](https://github.com/ibexa/core/blob/6.0/src/contracts/Repository/Values/Content/Query/Criterion/ContentName.php) searches for content by its name. ## Arguments - `value` - string representing the content name, the wildcard character `*` can be used for partial search ## Example ### PHP ```php use Ibexa\Contracts\Core\Repository\Values\Content\Query; use Ibexa\Contracts\Core\Repository\Values\Content\Query\Criterion; $query = new Query(); $query->query = new Criterion\ContentName('*phone'); ``` ### REST API **XML** ```xml *phone ``` **JSON** ```json "Query": { "Filter": { "ContentNameCriterion": "*phone" } } ``` # ContentTypeGroupId Criterion > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). ContentTypeGroupId Search Criterion The [`ContentTypeGroupId` Search Criterion](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-Core-Repository-Values-Content-Query-Criterion-ContentTypeGroupId.html) searches for content based on the ID of its content type group. ## Arguments - `value` - int(s) representing the content type group ID(s) ## Example ### PHP ```php use Ibexa\Contracts\Core\Repository\Values\Content\Query; use Ibexa\Contracts\Core\Repository\Values\Content\Query\Criterion; $query = new Query(); $query->query = new Criterion\ContentTypeGroupId([1, 2]); ``` ### REST API **XML** ```xml 1 ``` **JSON** ```json "Query": { "Filter": { "ContentTypeGroupIdCriterion": [1, 2] } } ``` ## Use case You can use the `ContentTypeGroupId` Criterion to query all Media content items (the default ID for the Media content type group is 3): ```php use Ibexa\Contracts\Core\Repository\SearchService; use Ibexa\Contracts\Core\Repository\Values\Content\Query; use Ibexa\Contracts\Core\Repository\Values\Content\Query\Criterion; $query = new Query(); $query->query = new Criterion\ContentTypeGroupId([3]); /** @var SearchService $searchService */ $results = $searchService->findContent($query); $media = []; foreach ($results->searchHits as $searchHit) { $media[] = $searchHit; } ``` # ContentTypeId Criterion > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). ContentTypeId Search Criterion The [`ContentTypeId` Search Criterion](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-Core-Repository-Values-Content-Query-Criterion-ContentTypeId.html) searches for content based on the ID of its content type. ## Arguments - `value` - int(s) representing the content type ID(s) ## Example ### PHP ```php use Ibexa\Contracts\Core\Repository\Values\Content\Query; use Ibexa\Contracts\Core\Repository\Values\Content\Query\Criterion; $query = new Query(); $query->query = new Criterion\ContentTypeId([44]); ``` ### REST API **XML** ```xml 44 ``` **JSON** ```json "Query": { "Filter": { "ContentTypeIdCriterion": 44 } } ``` # ContentTypeIdentifier Criterion > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). ContentTypeIdentifier Search Criterion The [`ContentTypeIdentifier` Search Criterion](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-Core-Repository-Values-Content-Query-Criterion-ContentTypeIdentifier.html) searches for content based on the identifier of its content type. ## Arguments - `value` - string(s) representing the content type identifier(s) ## Example ### PHP ```php use Ibexa\Contracts\Core\Repository\Values\Content\Query; use Ibexa\Contracts\Core\Repository\Values\Content\Query\Criterion; $query = new Query(); $query->query = new Criterion\ContentTypeIdentifier(['article', 'blog_post']); ``` ### REST API **XML** ```xml article ``` **JSON** ```json "Query": { "Filter": { "ContentTypeIdentifierCriterion": "article" } } ``` # CurrencyCode Criterion > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). CurrencyCode Search Criterion The `CurrencyCodeCriterion` Search Criterion searches for currencies by their codes. ## Arguments - `code` - string representing the currency code ## Limitations The `CurrencyCodeCriterion` Criterion isn't available in Solr or Elasticsearch engines. ## Example ### PHP ```php use Ibexa\Contracts\ProductCatalog\Values\Currency\CurrencyQuery; $query = new CurrencyQuery( new \Ibexa\Contracts\ProductCatalog\Values\Currency\Query\Criterion\CurrencyCodeCriterion('EUR') ); ``` # Custom Field Criterion > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). Custom Field Search Criterion The [`CustomField` Search Criterion](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-Core-Repository-Values-Content-Query-Criterion-CustomField.html) searches for content or locations based on the contents of the search index fields. The allowed syntax and operator support might differ between search engines and the type of queried field. ## Arguments - `target` - string representing the identifier of the search index field - `operator` - one of [Operator](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-Core-Repository-Values-Content-Query-Criterion-Operator.html) constants - `value` - the value to query for ## Limitations The `CustomField` Criterion isn't available in [Repository filtering](https://doc.ibexa.co/en/saas/search/search_api/#repository-filtering). ## Example ### PHP ```php query = new Query\Criterion\CustomField('content_name_s', Operator::EQ, '/Ibexa.*/'); /** @var \Ibexa\Contracts\Core\Repository\SearchService $searchService */ $searchService->findContent($query); ``` # CustomerGroupId Criterion > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). CustomerGroupId Search Criterion The `CustomerGroupId` Search Criterion searches for content based on the ID of its customer group. ## Arguments - `value` - int(s) representing the customer group ID(s) ## Example ### PHP ```php use Ibexa\Contracts\Core\Repository\Values\Content\Query; use Ibexa\Contracts\ProductCatalog\Values\Content\Query\Criterion; $query = new Query(); $query->query = new Criterion\CustomerGroupId(1); ``` # DateMetadata Criterion > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). DateMetadata Search Criterion The [`DateMetadata` Search Criterion](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-Core-Repository-Values-Content-Query-Criterion-DateMetadata.html) searches for content based on the date when it was created or last modified. ## Arguments - `target` - indicating if publication or modification date should be queried, either `DateMetadata::CREATED` or `DateMetadata::PUBLISHED` (both with the same functionality), or `DateMetadata::MODIFIED` - `operator` - Operator constant (IN, EQ, GT, GTE, LT, LTE, BETWEEN) - `value` - indicating the date(s) that should be matched, provided as a UNIX timestamp (or array of timestamps) ## Example ### PHP ```php use Ibexa\Contracts\Core\Repository\Values\Content\Query; use Ibexa\Contracts\Core\Repository\Values\Content\Query\Criterion; $query = new Query(); $query->query = new Criterion\DateMetadata( Criterion\DateMetadata::CREATED, Criterion\Operator::BETWEEN, [1576800000, 1576972800] ); ``` ### REST API **XML** ```xml modified 1675681020 gte ``` **JSON** ```json "Query": { "Filter": { "DateMetadataCriterion": { "Target": "modified", "Value": 1675681020, "Operator": "gte" } } } ``` ## Use case You can use the `DateMetadata` Criterion to search for blog posts that have been created within the last week: ```php use Ibexa\Contracts\Core\Repository\Values\Content\LocationQuery; use Ibexa\Contracts\Core\Repository\Values\Content\Query\Criterion; $query = new LocationQuery(); $date = strtotime('-1 week'); $query->query = new Criterion\LogicalAnd( [ new Criterion\ContentTypeIdentifier('blog_post'), new Criterion\DateMetadata(Criterion\DateMetadata::CREATED, Criterion\Operator::GTE, $date), ] ); ``` # Depth Criterion > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). Depth Search Criterion The [`Location\Depth` Search Criterion](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-Core-Repository-Values-Content-Query-Criterion-Location-Depth.html) searches for locations based on their depth in the content tree. This Criterion is available only for Location Search. ## Arguments - `operator` - Operator constant (IN, EQ, GT, GTE, LT, LTE, BETWEEN) - `value` - int(s) representing the location depth(s) The `value` argument requires: - a list of ints for `Operator::IN` - exactly two ints for `Operator::BETWEEN` - a single int for other Operators ## Example ### PHP ```php use Ibexa\Contracts\Core\Repository\Values\Content\Query; use Ibexa\Contracts\Core\Repository\Values\Content\Query\Criterion; $query = new Query(); $query->query = new Criterion\Location\Depth(Criterion\Operator::LT, 3); ``` # Field Criterion > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). Field Search Criterion The [`Field` Search Criterion](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-Core-Repository-Values-Content-Query-Criterion-Field.html) searches for content based on the content of one of its fields. ## Arguments - `target` - string representing the identifier of the field to query - `operator` - operator constant (IN, EQ, GT, GTE, LT, LTE, LIKE, BETWEEN, CONTAINS) - `value` - the value to query for The `LIKE` operator works together with wildcards (`*`). Without a wildcards its results are the same as for the `EQ` operator. The `CONTAINS` operator works with collection fields like the Country field type, enabling you to retrieve results when the query value is one of the values of the collection. Querying for a collection with the `EQ` operator returns result only when the whole collection equals the query values. ## Limitations The `Field` Criterion isn't available in [Repository filtering](https://doc.ibexa.co/en/saas/search/search_api/#repository-filtering). ## Example ### PHP ```php use Ibexa\Contracts\Core\Repository\Values\Content\Query; use Ibexa\Contracts\Core\Repository\Values\Content\Query\Criterion; $query = new Query(); $query->query = new Criterion\Field('name', Criterion\Operator::CONTAINS, 'Platform'); ``` ### REST API **XML** ```xml name CONTAINS Platform ``` **JSON** ```json { "Query": { "Filter": { "Field": { "name": "name", "operator": "CONTAINS", "value": "Platform" } } } } ``` ## Use case You can use the `Field` Criterion to search for articles that contain the word "featured": ```php use Ibexa\Contracts\Core\Repository\Values\Content\LocationQuery; use Ibexa\Contracts\Core\Repository\Values\Content\Query\Criterion; $query = new LocationQuery(); $query->query = new Criterion\LogicalAnd( [ new Criterion\ContentTypeIdentifier('article'), new Criterion\Field('name', Criterion\Operator::CONTAINS, 'Featured'), ] ); ``` # FieldRelation Criterion > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). FieldRelation Search Criterion The [`FieldRelation` Search Criterion](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-Core-Repository-Values-Content-Query-Criterion-FieldRelation.html) searches for content based on the content items it has Relations to. ## Arguments - `target` - string representing the identifier of the Field containing Relations - `operator` - Operator constant (IN, EQ, GT, GTE, LT, LTE, BETWEEN) - `value` - array of ints representing the Relation content IDs to search for Use of IN means the Relation needs to have one of the provided IDs, while CONTAINS implies it needs to have all provided IDs. ## Limitations The `FieldRelation` Criterion isn't available in [Repository filtering](https://doc.ibexa.co/en/saas/search/search_api/#repository-filtering). ## Example ### PHP ```php use Ibexa\Contracts\Core\Repository\Values\Content\Query; use Ibexa\Contracts\Core\Repository\Values\Content\Query\Criterion; $query = new Query(); $query->query = new Criterion\FieldRelation('relations', Criterion\Operator::CONTAINS, [55, 63]); ``` # Full-Text Criterion > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). Full-Text Search Criterion The [`FullText` Search Criterion](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-Core-Repository-Values-Content-Query-Criterion-FullText.html) searches for content based on the full text content of its fields. ## Arguments - `value` - string to search for ## Supported syntax | Feature | Elasticsearch | Apache Solr | Legacy Search Engine (SQL) | | --------------------------------- | ------------- | ----------- | -------------------------- | | Boolean operators: AND (&&), OR ( | | ), NOT (!) | No\* | | Require/exclude operators: +, - | No | Yes | No | | Grouping with parentheses | No | Yes | No | | Phrase search with double quotes | No | Yes | No | | Asterisks (\*) as wildcards | No | Yes | Yes, limited\*\*\* | \* When using the Elasticsearch search engine, a full text query performs an OR query by default, while the OR and AND operators return unexpected results. \*\* When using the Legacy search engine, a full text query performs an OR query. \*\*\* Asterisk may only be located at the beginning or end of a query. ## Limitations When using the Legacy search engine, a full text query performs an OR query by default, and supports asterisks as wildcards located at the beginning or end of a query. When using the Elasticsearch search engine, a full text query performs an OR query by default, while the OR and AND operators return unexpected results. The `FullText` Criterion isn't available in [Repository filtering](https://doc.ibexa.co/en/saas/search/search_api/#repository-filtering). ## Example ### PHP ```php use Ibexa\Contracts\Core\Repository\Values\Content\Query; use Ibexa\Contracts\Core\Repository\Values\Content\Query\Criterion; $query = new Query(); $query->query = new Criterion\FullText('victory'); ``` Using double quotes to indicate a phrase: ```php use Ibexa\Contracts\Core\Repository\Values\Content\Query; use Ibexa\Contracts\Core\Repository\Values\Content\Query\Criterion; $query = new Query(); $query->query = new Criterion\FullText('"world cup"'); ``` Using the AND operator and parenthesis to search for both words at the same time: ```php use Ibexa\Contracts\Core\Repository\Values\Content\Query; use Ibexa\Contracts\Core\Repository\Values\Content\Query\Criterion; $query = new Query(); $query->query = new Criterion\FullText('baseball AND cup'); ``` ### REST API **XML** ```xml victory ``` **JSON** ```json "Query": { "Filter": { "FullTextCriterion": "victory" } } ``` ## Use cases Assume the following search query: ```php use Ibexa\Contracts\Core\Repository\Values\Content\Query; use Ibexa\Contracts\Core\Repository\Values\Content\Query\Criterion; $query = new Query(); $query->query = new Criterion\FullText('(cup AND ba*ball) "breaking news"'); ``` It returns content containing phrases such as "Breaking news", "Baseball world cup", "Basketball cup", or "Breaking news: Baseball world cup victory". It doesn't return content with phrases such as "Football world cup" or "Breaking sports news". # Image Criterion > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). Image Search Criterion The `Image` Search Criterion searches for image by specified image attributes. ## Arguments - `fieldDefIdentifier` - string representing the identifier of the field - `imageCriteriaData` - array representing image attributes. All attributes are optional. ## Example ### PHP ```php use Ibexa\Contracts\Core\Repository\Values\Content\Query; use Ibexa\Contracts\Core\Repository\Values\Content\Query\Criterion; $imageCriteriaData = [ 'mimeTypes' => [ 'image/png', ], 'orientation' => [ 'image/png', ], 'width' => [ 'min' => 0, // (default: 0, optional) 'max' => 1000, // (default: null, optional) ], 'height' => [ 'min' => 0, // (default: 0, optional) 'max' => 1000, // (default: null, optional) ], 'size' => [ 'min' => 0, // (default: 0, optional) 'max' => 2, // (default: null, optional) ], ]; $query = new Query(); $query->query = new Criterion\Image('image', $imageCriteriaData); ``` ### REST API **XML** ```xml image image/png 0 2 100 1000 500 1500 portrait ``` **JSON** ```json "Query": { "Filter": { "ImageCriterion": { "fieldDefIdentifier": "image", "mimeTypes": "image/png", "size": { "max": 1.5 }, "width": { "max": 1000 }, "height": { "max": 1500 }, "orientation": "portrait" } } } OR "Query": { "Filter": { "ImageCriterion": { "fieldDefIdentifier": "image", "mimeTypes": [ "image/png", "image/jpeg" ], "size": { "min": 0, "max": 2 }, "width": { "min": 100, "max": 1000 }, "height": { "min": 500, "max": 1500 }, "orientation": [ "portrait", "landscape" ] } } } ``` # Image Dimension Criterion > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). Image Dimensions Search Criterion The `Dimensions` Search Criterion searches for image with specified dimensions. ## Arguments - `fieldDefIdentifier` - string representing the identifier of the field - `imageCriteriaData` - an array representing minimum and maximum values for width and height, expressed in pixels ## Example ### PHP ```php use Ibexa\Contracts\Core\Repository\Values\Content\Query; use Ibexa\Contracts\Core\Repository\Values\Content\Query\Criterion; $query = new Query(); $imageCriteriaData = [ 'width' => [ 'min' => 100, // (default: 0, optional) 'max' => 1000, // (default: null, optional) ], 'height' => [ 'min' => 500, // (default: 0, optional) 'max' => 1500, // (default: null, optional) ], ]; $query->query = new Criterion\Image\Dimensions('image', $imageCriteriaData); ``` ### REST API **XML** ```xml image 100 1000 500 1500 ``` **JSON** ```json "Query": { "Filter": { "ImageDimensionsCriterion": { "fieldDefIdentifier": "image", "width": { "min": 100, "max": 1000 }, "height": { "min": 500, "max": 1500 } } } } ``` # Image FileSize Criterion > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). Image FileSize Search Criterion The `FileSize` Search Criterion searches for image with specified size. ## Arguments - `fieldDefIdentifier` - string representing the identifier of the field - (optional) `minValue` - numeric representing minimum file size expressed in MB, default: 0 - (optional) `maxValue` - numeric representing maximum file size expressed in MB, default: `null` ## Example ### PHP ```php use Ibexa\Contracts\Core\Repository\Values\Content\Query; use Ibexa\Contracts\Core\Repository\Values\Content\Query\Criterion; $query = new Query(); $query->query = new Criterion\Image\FileSize('image', 0, 1.5); ``` ### REST API **XML** ```xml image 0 1.5 ``` **JSON** ```json "Query": { "Filter": { "ImageFileSizeCriterion":{ "fieldDefIdentifier": "image", "size": { "min": 0, "max": 1.5 } } } } ``` # Image Height Criterion > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). Image Height Search Criterion The `Height` Search Criterion searches for image with specified height. ## Arguments - `fieldDefIdentifier` - string representing the identifier of the field - (optional) `minValue` - int representing minimum file height expressed in pixels, default: 0 - (optional) `maxValue` - int representing maximum file height expressed in pixels, default: `null` ## Example ### PHP ```php use Ibexa\Contracts\Core\Repository\Values\Content\Query; use Ibexa\Contracts\Core\Repository\Values\Content\Query\Criterion; $query = new Query(); $query->query = new Criterion\Image\Height('image', 0, 1500); ``` # Image MimeType Criterion > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). Image MimeType Search Criterion The `MimeType` Search Criterion searches for image with specified mime type(s). ## Arguments - `fielDefIdentifier` - string representing the identifier of the field - `type` - string(s) representing mime type(s) ## Example ### PHP ```php use Ibexa\Contracts\Core\Repository\Values\Content\Query; use Ibexa\Contracts\Core\Repository\Values\Content\Query\Criterion; $query = new Query(); $query->query = new Criterion\Image\MimeType('image', 'image/jpeg'); ``` or ```php use Ibexa\Contracts\Core\Repository\Values\Content\Query; use Ibexa\Contracts\Core\Repository\Values\Content\Query\Criterion; $query = new Query(); $mimeTypes = [ 'image/jpeg', 'image/png', ]; $query->query = new Criterion\Image\MimeType('image', $mimeTypes); ``` ### REST API **XML** ```xml image image/png ``` **JSON** ```json "Query": { "Filter": { "ImageMimeTypeCriterion": { "fieldDefIdentifier": "image", "type": "image/png" } } } OR "Query": { "Filter": { "ImageMimeTypeCriterion": { "fieldDefIdentifier": "image", "type": ["image/png", "image/jpeg"] } } } ``` # Image Orientation Criterion > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). Image Orientation Search Criterion The `Orientation` Search Criterion searches for image with specified orientation(s). Supported orientation values: landscape, portrait and square. ## Arguments - `fielDefIdentifier` - string representing the identifier of the field - `orientation` - strings representing orientations ## Example ### PHP #### Single orientation value ```php use Ibexa\Contracts\Core\Repository\Values\Content\Query; use Ibexa\Contracts\Core\Repository\Values\Content\Query\Criterion; use Ibexa\Contracts\Core\Repository\Values\Content\Query\Criterion\Image\Orientation; $query = new Query(); $query->query = new Orientation('image', 'landscape'); ``` #### Multiple orientation values ```php use Ibexa\Contracts\Core\Repository\Values\Content\Query; use Ibexa\Contracts\Core\Repository\Values\Content\Query\Criterion\Image\Orientation; $query = new Query(); $orientations = [ 'landscape', 'portrait', ]; $query->query = new Orientation('image', $orientations); ``` ### REST API **XML** ```xml image landscape ``` **JSON** ```json "Query": { "Filter": { "ImageOrientationCriterion": { "fieldDefIdentifier": "image", "orientation": "landscape" } } } OR "Query": { "Filter": { "ImageOrientationCriterion": { "fieldDefIdentifier": "image", "orientation": ["portrait", "landscape"] } } } ``` # Image Width Criterion > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). Image Width Search Criterion The `Width` Search Criterion searches for image with specified width. ## Arguments - `fieldDefIdentifier` - string representing the identifier of the field - (optional) `minValue` - int representing minimum file width expressed in pixels, default: 0 - (optional) `maxValue` - int representing maximum file width expressed in pixels, default: `null` ## Example ### PHP ```php use Ibexa\Contracts\Core\Repository\Values\Content\Query; use Ibexa\Contracts\Core\Repository\Values\Content\Query\Criterion; $query = new Query(); $query->query = new Criterion\Image\Width('image', 150, 1000); ``` # IsBookmarked Criterion > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). IsBookmarked Search Criterion The [`IsBookmarked` Search Criterion](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-Core-Repository-Values-Content-Query-Criterion-Location-IsBookmarked.html) searches for location based on whether it's bookmarked or not. It works with current user reference. This Criterion is available only for location Search. ## Arguments - `value` - bool representing whether to search for bookmarked location (default `true`) or not bookmarked location (`false`) ## Example ### PHP ```php use Ibexa\Contracts\Core\Repository\Values\Content\LocationQuery; use Ibexa\Contracts\Core\Repository\Values\Content\Query\Criterion\Location\IsBookmarked; $query = new LocationQuery(); $query->filter = new IsBookmarked(); /** @var \Ibexa\Contracts\Core\Repository\SearchService $searchService */ $results = $searchService->findLocations($query); ``` ### REST API **XML** ```xml true ``` **JSON** ```json "Query": { "Filter": { "IsBookmarkedCriterion": true } } ``` # IsContainer Criterion > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). IsContainer Search Criterion The [`IsContainer` Search Criterion](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-Core-Repository-Values-Content-Query-Criterion-IsContainer.html) searches for content items based on whether they are containers (i.e., can contain other content items). ## Arguments - `value` – boolean (optional, default: `true`). If `true`, searches for content that is a container. If `false`, searches for content that is not a container. ## Example ### PHP ```php use Ibexa\Contracts\Core\Repository\Values\Content\Query; use Ibexa\Contracts\Core\Repository\Values\Content\Query\Criterion; $query = new Query(); $query->query = new Criterion\IsContainer(); // Finds containers $query->query = new Criterion\IsContainer(false); // Finds non-containers ``` # IsCurrencyEnabledCriterion Criterion > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). IsCurrencyEnabledCriterion Search Criterion The `IsCurrencyEnabledCriterion` Search Criterion searches for currencies that are enabled in the system. ## Arguments - (optional) `enabled` - bool representing whether to search for enabled (default `true`), or disabled Currencies (`false`) ## Limitations The `IsCurrencyEnabledCriterion` Criterion isn't available in Solr or Elasticsearch engines. ## Example ### PHP ```php use Ibexa\Contracts\ProductCatalog\Values\Currency\CurrencyQuery; $query = new CurrencyQuery( new \Ibexa\Contracts\ProductCatalog\Values\Currency\Query\Criterion\IsCurrencyEnabledCriterion() ); ``` # IsFieldEmpty Criterion > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). IsFieldEmpty Search Criterion The [`IsFieldEmpty` Search Criterion](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-Core-Repository-Values-Content-Query-Criterion-IsFieldEmpty.html) searches for content based on whether a specified field is empty or not. ## Arguments - `fieldDefinitionIdentifier` - string representing the identifier of the field - (optional) `value` - bool representing whether to search for empty (default `true`), or non-empty fields (`false`) ## Limitations The `IsFieldEmpty` Criterion isn't available in [Repository filtering](https://doc.ibexa.co/en/saas/search/search_api/#repository-filtering). The Richtext field type (`ibexa_richtext`) isn't searchable in the Legacy search engine. The `IsFieldEmpty` criterion doesn't work for [Taxonomy entry assignment](https://doc.ibexa.co/en/saas/content_management/field_types/field_type_reference/taxonomyentryassignmentfield/index.md) fields. For this use case, use [`TaxonomyNoEntries`](https://doc.ibexa.co/en/saas/search/criteria_reference/taxonomy_no_entries/index.md) instead. ## Example ### PHP ```php use Ibexa\Contracts\Core\Repository\Values\Content\Query; use Ibexa\Contracts\Core\Repository\Values\Content\Query\Criterion; $query = new Query(); $query->query = new Criterion\IsFieldEmpty('title'); ``` ## Use case You can use the `IsFieldEmpty` Criterion to search for articles that don't have an image: ```php use Ibexa\Contracts\Core\Repository\Values\Content\LocationQuery; use Ibexa\Contracts\Core\Repository\Values\Content\Query\Criterion; $query = new LocationQuery(); $query->query = new Criterion\LogicalAnd( [ new Criterion\ContentTypeIdentifier('article'), new Criterion\IsFieldEmpty('image'), ] ); ``` # IsMainLocation Criterion > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). IsMainLocation Search Criterion The [`IsMainLocation` Search Criterion](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-Core-Repository-Values-Content-Query-Criterion-LanguageCode.html) searches for locations based on whether they're the main location of a content item or not. This Criterion is available only for Location Search. ## Arguments - `value` - `IsMainLocation::MAIN` (0) or `IsMainLocation::NOT_MAIN` (1), representing whether to search for a main or not main location ## Example ### PHP ```php use Ibexa\Contracts\Core\Repository\Values\Content\Query; use Ibexa\Contracts\Core\Repository\Values\Content\Query\Criterion; use Ibexa\Contracts\Core\Repository\Values\Content\Query\Criterion\Location\IsMainLocation; $query = new Query(); $query->query = new Criterion\Location\IsMainLocation(IsMainLocation::MAIN); ``` # IsProductBased Criterion > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). IsProductBased Search Criterion The `IsProductBased` Search Criterion searches for content that plays the role of a Product. ## Example ### PHP ```php use Ibexa\Contracts\Core\Repository\Values\Content\Query; use Ibexa\Contracts\Core\Repository\Values\Content\Query\Criterion; $query = new Query(); $query->query = new \Ibexa\Contracts\ProductCatalog\Values\Content\Query\Criterion\IsProductBased(); ``` # IsUserBased Criterion > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). IsUserBased Search Criterion The [`IsUserBased` Search Criterion](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-Core-Repository-Values-Content-Query-Criterion-IsUserBased.html) searches for content that plays the role of a User account. > **Note: Note** > > In the default setup only the user content type is treated as user accounts. However, you can also [set other content types to be treated as such](https://doc.ibexa.co/en/saas/administration/configuration/repository_configuration/#user-identifiers). ## Arguments - (optional) `value` - bool representing whether to search for User-based (default `true`) or non-User-based content ## Limitations The `IsUserBased` Criterion isn't available in Solr or Elasticsearch engines. ## Example ### PHP ```php use Ibexa\Contracts\Core\Repository\Values\Content\Query; use Ibexa\Contracts\Core\Repository\Values\Content\Query\Criterion; $query = new Query(); $query->query = new Criterion\IsUserBased(); ``` ### REST API **XML** ```xml false ``` **JSON** ```json "Query": { "Filter": { "IsUserBasedCriterion": "false" } } ``` # IsUserEnabled Criterion > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). IsUserEnabled Search Criterion The [`IsUserEnabled` Search Criterion](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-Core-Repository-Values-Content-Query-Criterion-IsUserEnabled.html) searches for user accounts that are enabled or disabled. ## Arguments - (optional) `value` - bool representing whether to search for enabled (default `true`) or disabled user accounts ## Example ### PHP ```php use Ibexa\Contracts\Core\Repository\Values\Content\Query; use Ibexa\Contracts\Core\Repository\Values\Content\Query\Criterion; $query = new Query(); $query->query = new Criterion\IsUserEnabled(); ``` ### REST API **XML** ```xml true ``` **JSON** ```json "Query": { "Filter": { "IsUserEnabledCriterion": "true" } } ``` # LanguageCode Criterion > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). LanguageCode Search Criterion The [`LanguageCode` Search Criterion](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-Core-Repository-Values-Content-Query-Criterion-Location.html) searches for content based on whether it's translated into the selected language. ## Arguments - `value` - string(s) representing the language codes to search for - (optional) `matchAlwaysAvailable` - bool representing whether content with the `alwaysAvailable` flag should be returned even if it doesn't contain the selected language (default `true`) ## Example ### PHP ```php use Ibexa\Contracts\Core\Repository\Values\Content\Query; use Ibexa\Contracts\Core\Repository\Values\Content\Query\Criterion; $query = new Query(); $query->query = new Criterion\LanguageCode('ger-DE', false); ``` ### REST API **XML** ```xml eng-GB ``` **JSON** ```json "Query": { "Filter": { "LanguageCodeCriterion": "eng-GB" } } ``` ## Use case You can use the `LanguageCode` Criterion to search for articles that are lacking a translation into a specific language: ```php use Ibexa\Contracts\Core\Repository\Values\Content\LocationQuery; use Ibexa\Contracts\Core\Repository\Values\Content\Query\Criterion; $query = new LocationQuery(); $query->query = new Criterion\LogicalAnd( [ new Criterion\ContentTypeIdentifier('article'), new Criterion\LogicalNot( new Criterion\LanguageCode('ger-DE', false) ), ] ); /** @var \Ibexa\Contracts\Core\Repository\SearchService $searchService */ $results = $searchService->findContent($query); $articlesToTranslate = []; foreach ($results->searchHits as $searchHit) { $articlesToTranslate[] = $searchHit; } return $articlesToTranslate; ``` # LocationId Criterion > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). LocationId Search Criterion The [`LocationId` Search Criterion](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-Core-Repository-Values-Content-Query-Criterion-LocationId.html) searches for content based in the location ID. ## Arguments - `value` - int(s) representing the location ID(s) ## Example ### PHP ```php use Ibexa\Contracts\Core\Repository\Values\Content\Query; use Ibexa\Contracts\Core\Repository\Values\Content\Query\Criterion; $query = new Query(); $query->query = new Criterion\LocationId(62); ``` ### REST API **XML** ```xml 62 ``` **JSON** ```json "Query": { "Filter": { "LocationIdCriterion": "62" } } ``` # LocationRemoteId Criterion > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). LocationRemoteId Search Criterion The [`LocationRemoteId` Search Criterion](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-Core-Repository-Values-Content-Query-Criterion-LocationRemoteId.html) searches for content based in the location remote ID. ## Arguments - `value` - string(s) representing the location remote ID(s) ## Example ### PHP ```php use Ibexa\Contracts\Core\Repository\Values\Content\Query; use Ibexa\Contracts\Core\Repository\Values\Content\Query\Criterion; $query = new Query(); $query->query = new Criterion\LocationRemoteId(['4d1e5f216c0a7aaab7f005ffd4b6a8a8', 'b81ef3e62b514188bfddd2a80d447d34']); ``` ### REST API **XML** ```xml 3aaeefdb0ae573ac91f6d6ea78d230b7 ``` **JSON** ```json "Query": { "Filter": { "LocationRemoteIdCriterion": "3aaeefdb0ae573ac91f6d6ea78d230b7" } } ``` # MapLocationDistance Criterion > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). MapLocationDistance Search Criterion The [`MapLocationDistance` Search Criterion](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-Core-Repository-Values-Content-Query-Criterion-MapLocationDistance.html) searches content based on the distance between the location contained in a MapLocation field and the provided coordinates. ## Arguments - `target` - string representing the field definition identifier - `operator` - Operator constant (IN, EQ, GT, GTE, LT, LTE, BETWEEN) - `distance` - float(s) representing the distances between the map location in the field and the location provided in `latitude` and `longitude` arguments - `latitude` - float representing the latitude of the location to calculate distance to - `longitude` - float representing the longitude of the location to calculate distance to The `distance` argument requires: - a list of floats for `Operator::IN` or `Operator::BETWEEN` - a single float for other Operators ## Limitations The `MapLocationDistance` Criterion isn't available in [Repository filtering](https://doc.ibexa.co/en/saas/search/search_api/#repository-filtering). ## Example ### PHP ```php use Ibexa\Contracts\Core\Repository\Values\Content\Query; use Ibexa\Contracts\Core\Repository\Values\Content\Query\Criterion; $query = new Query(); $query->query = new Criterion\MapLocationDistance('location', Criterion\Operator::LTE, 5, 51.395973, 22.531696); ``` # MatchAll Criterion > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). MatchAll Search Criterion The [`MatchAll` content](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-Core-Repository-Values-Content-Query-Criterion-MatchAll.html) and [`MatchAll` product](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-ProductCatalog-Values-Product-Query-Criterion-MatchAll.html) search criteria are auxiliary criteria that returns all search results. They're used internally when no filter or query is provided on a Query object. The criteria take no arguments. # MatchNone Criterion > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). MatchNone Search Criterion The [`MatchNone` Search Criterion](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-Core-Repository-Values-Content-Query-Criterion-MatchNone.html) is an auxiliary Criterion that returns no search results. It's used internally when no filter or query is provided on a Query object. The Criterion takes no arguments. # ObjectStateId Criterion > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). ObjectStateId Search Criterion The [`ObjectStateId` Search Criterion](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-Core-Repository-Values-Content-Query-Criterion-ObjectStateId.html) searches for content based on its object state ID. ## Arguments - `value` - int(s) representing the object state ID(s) ## Example ### PHP ```php use Ibexa\Contracts\Core\Repository\Values\Content\Query; use Ibexa\Contracts\Core\Repository\Values\Content\Query\Criterion; $query = new Query(); $query->query = new Criterion\ObjectStateId([4, 5]); ``` ### REST API **XML** ```xml 1 ``` **JSON** ```json "Query": { "Filter": { "ObjectStateIdCriterion": "1" } } ``` # ObjectStateIdentifier Criterion > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). ObjectStateIdentifier Search Criterion The [`ObjectStateIdentifier` Search Criterion](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-Core-Repository-Values-Content-Query-Criterion-ObjectStateId.html) searches for content based on its object state identifier. ## Arguments - `value` - string(s) representing the object state identifier(s) - `target` (optional for PHP) - string representing the object state group ## Example ### PHP ```php use Ibexa\Contracts\Core\Repository\Values\Content\Query; use Ibexa\Contracts\Core\Repository\Values\Content\Query\Criterion; $query = new Query(); $query->query = new Criterion\ObjectStateIdentifier(['ready']); ``` ```php use Ibexa\Contracts\Core\Repository\Values\Content\Query; use Ibexa\Contracts\Core\Repository\Values\Content\Query\Criterion; $query = new Query(); $query->query = new Criterion\ObjectStateIdentifier(['not_locked'], 'ibexa_lock'); ``` ### REST API **XML** ```xml not_locked ibexa_lock ``` **JSON** ```json { "Query": { "Filter": { "ObjectStateIdentifierCriterion": { "value": "not_locked", "target": "ibexa_lock" } } } } ``` # ParentLocationId Criterion > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). ParentLocationId Search Criterion The [`ParentLocationId` Search Criterion](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-Core-Repository-Values-Content-Query-Criterion-ParentLocationId.html) searches for content based on the Location ID of its parent. ## Arguments - `value` - int(s) representing the parent location IDs ## Example ### PHP ```php use Ibexa\Contracts\Core\Repository\Values\Content\Query; use Ibexa\Contracts\Core\Repository\Values\Content\Query\Criterion; $query = new Query(); $query->query = new Criterion\ParentLocationId([54, 58]); ``` ### REST API **XML** ```xml [81, 82] ``` **JSON** ```json "Query": { "Filter": { "ParentLocationIdCriterion": [69, 72] } } ``` ## Use case You can use the `ParentLocationId` Search Criterion to list blog posts contained in a blog: ```php use Ibexa\Contracts\Core\Repository\Values\Content\LocationQuery; use Ibexa\Contracts\Core\Repository\Values\Content\Query\Criterion; $locationId = 12345; $query = new LocationQuery(); $query->query = new Criterion\LogicalAnd([ new Criterion\Visibility(Criterion\Visibility::VISIBLE), new Criterion\ParentLocationId($locationId), ]); /** @var \Ibexa\Contracts\Core\Repository\SearchService $searchService */ $results = $searchService->findLocations($query); $posts = []; foreach ($results->searchHits as $searchHit) { $posts[] = $searchHit; } return $this->render('full/blog.html.twig', [ 'posts' => $posts, ]); ``` ```html+twig

    Posts:

      {% for post in posts %}
    • {{ post.valueObject.contentInfo.name }}
    • {% endfor %}
    ``` # ParentLocationRemoteId Criterion > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). ParentLocationRemoteId Search Criterion The `ParentLocationRemoteId` Search Criterion searches for content based on the location remote ID of its parent. ## Arguments - `value` - int(s) representing the parent location remote IDs ### REST API **XML** ```xml abab615dcf26699a4291657152da4337 ``` **JSON** ```json "Query": { "Filter": { "ParentLocationRemoteIdCriterion": "abab615dcf26699a4291657152da4337" } } ``` # Priority Criterion > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). Priority Search Criterion The [`Location\Priority` Search Criterion](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-Core-Repository-Values-Content-Query-Criterion-Location-Priority.html) searches for locations based on their priority. This Criterion is available only for Location Search. ## Arguments - `operator`- Operator constant (GT, GTE, LT, LTE, BETWEEN) - `value` - int(s) representing the priority The `value` argument requires: - a list of ints for `Operator::BETWEEN` - a single int for other Operators ## Example ### PHP ```php use Ibexa\Contracts\Core\Repository\Values\Content\Query; use Ibexa\Contracts\Core\Repository\Values\Content\Query\Criterion; $query = new Query(); $query->query = new Criterion\Location\Priority(Criterion\Operator::GTE, 50); ``` # RemoteId / ContentRemoteId Criterion > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). RemoteId / ContentRemoteId Search Criterion The [`RemoteId` / `ContentRemoteId` Search Criterion](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-Core-Repository-Values-Content-Query-Criterion-RemoteId.html) searches for content based on its remote content ID. ## Arguments - `value` - string(s) representing the remote IDs ## Example ### PHP ```php use Ibexa\Contracts\Core\Repository\Values\Content\Query; use Ibexa\Contracts\Core\Repository\Values\Content\Query\Criterion; $query = new Query(); $query->query = new Criterion\RemoteId('abab615dcf26699a4291657152da4337'); ``` ### REST API **XML** ```xml abab615dcf26699a4291657152da4337 ``` **JSON** ```json "Query": { "Filter": { "ContentRemoteIdCriterion": "abab615dcf26699a4291657152da4337" } } ``` # SectionId Criterion > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). SectionId Search Criterion The [`SectionId` Search Criterion](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-Core-Repository-Values-Content-Query-Criterion-SectionId.html) searches for content based on the ID of the Section it's assigned to. ## Arguments - `value` - int(s) representing the IDs of the Section(s) ## Example ### PHP ```php use Ibexa\Contracts\Core\Repository\Values\Content\Query; use Ibexa\Contracts\Core\Repository\Values\Content\Query\Criterion; $query = new Query(); $query->query = new Criterion\SectionId(3); ``` ### REST API **XML** ```xml 3 ``` **JSON** ```json "Query": { "Filter": { "SectionIdCriterion": "3" } } ``` # SectionIdentifier Criterion > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). SectionIdentifier Search Criterion The [`SectionIdentifier` Search Criterion](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-Core-Repository-Values-Content-Query-Criterion-SectionIdentifier.html) searches for content based on the identifier of the Section it's assigned to. ## Arguments - `value` - string(s) representing the identifiers of the Section(s) ## Example ### PHP ```php use Ibexa\Contracts\Core\Repository\Values\Content\Query; use Ibexa\Contracts\Core\Repository\Values\Content\Query\Criterion; $query = new Query(); $query->query = new Criterion\SectionIdentifier(['sports', 'news']); ``` ### REST API **XML** ```xml sports ``` **JSON** ```json "Query": { "Filter": { "SectionIdentifierCriterion": "sports" } } ``` # Sibling Criterion > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). Sibling Search Criterion The [`Sibling` Search Criterion](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-Core-Repository-Values-Content-Query-Criterion-Sibling.html) searches for content under the same parent as the indicated location. ## Arguments - `locationId` - int representing the location ID - `parentLocationId` - int representing the parent location ID ## Example ### PHP ```php use Ibexa\Contracts\Core\Repository\Values\Content\Query; use Ibexa\Contracts\Core\Repository\Values\Content\Query\Criterion; $query = new Query(); $query->query = new Criterion\Sibling(59, 2); ``` You can also use the named constructor `Criterion\Sibling::fromLocation` and provide it with the location object: ```php use Ibexa\Contracts\Core\Repository\Values\Content\Query; use Ibexa\Contracts\Core\Repository\Values\Content\Query\Criterion; $query = new Query(); /** @var \Ibexa\Contracts\Core\Repository\LocationService $locationService */ $location = $locationService->loadLocation(59); $query->query = Criterion\Sibling::fromLocation($location); ``` ### REST API ### REST API **XML** ```xml 85 81 ``` **JSON** ```json "Query": { "Filter": { "SiblingCriterion": { "locationId": 85, "parentLocationId": 81 } } } ``` # Subtree Criterion > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). Subtree Search Criterion The [`Subtree` Search Criterion](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-Core-Repository-Values-Content-Query-Criterion-Subtree.html) searches for content based on its location ID subtree path. It returns the content item and all the content items below it in the subtree. ## Arguments - `value` - string(s) representing the pathstring(s) to search for ## Example ### PHP ```php use Ibexa\Contracts\Core\Repository\Values\Content\Query; use Ibexa\Contracts\Core\Repository\Values\Content\Query\Criterion; $query = new Query(); $query->query = new Criterion\Subtree('/1/2/71/72/'); ``` ### REST API **XML** ```xml /1/2/71/ ``` **JSON** ```json "Query": { "Filter": { "SubtreeCriterion": "/1/2/71/" } } ``` # TaxonomyEntryId Criterion > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). TaxonomyEntryId Search Criterion The [`TaxonomyEntryId` Search Criterion](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-Taxonomy-Search-Query-Criterion-TaxonomyEntryId.html) searches for content based on the ID of the Taxonomy Entry it's assigned to. ## Arguments - `value` - int(s) representing the IDs of the Tag(s) ## Example ### PHP ```php use Ibexa\Contracts\Core\Repository\Values\Content\Query; use Ibexa\Contracts\Taxonomy\Search\Query\Criterion; $query = new Query(); $query->query = new Criterion\TaxonomyEntryId(1); ``` Add an array of ID's to find Content tagged with at least one of the tags (OR). ```php use Ibexa\Contracts\Core\Repository\Values\Content\Query; use Ibexa\Contracts\Taxonomy\Search\Query\Criterion; $query = new Query(); $query->query = new Criterion\TaxonomyEntryId([1, 2, 3]); ``` # TaxonomyNoEntries Criterion > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). TaxonomyNoEntries Search Criterion The [`TaxonomyNoEntries`](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-Taxonomy-Search-Query-Criterion-TaxonomyNoEntries.html) Search Criterion searches for content that has no entries assigned from the specified [taxonomy](https://doc.ibexa.co/en/saas/content_management/taxonomy/taxonomy/index.md). Use it when you need to find content items to which no taxonomy entries have been assigned (for example, articles without tags). It's available for all supported search engines and in [repository filtering](https://doc.ibexa.co/en/saas/search/search_api/#repository-filtering). ## Arguments - `taxonomy` - `string` representing the identifier of the taxonomy (for example, `tags` or `categories`) ## Example ### PHP The following example searches for articles that have no entries assigned in the `tags` taxonomy: ```php query = new LogicalAnd( [ new TaxonomyNoEntries('tags'), new ContentTypeIdentifier('article'), ] ); /** @var \Ibexa\Contracts\Core\Repository\SearchService $searchService */ $results = $searchService->findContent($query); ``` The criteria limit the results to content that matches all of the conditions listed below: - content has no entries assigned in the `tags` taxonomy - content type is `article` # TaxonomySubtree Criterion > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). TaxonomySubtree Search Criterion The [`TaxonomySubtree`](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-Taxonomy-Search-Query-Criterion-TaxonomySubtree.html) Search Criterion searches for content assigned to the specified [taxonomy](https://doc.ibexa.co/en/saas/content_management/taxonomy/taxonomy/index.md) entry or any of its descendants. ## Arguments - `taxonomyEntryId` - `int` representing the ID of the taxonomy entry that is the root of the subtree ## Example ### PHP The following example searches for articles assigned to taxonomy entry with ID `42` or any of its child entries: ```php query = new LogicalAnd( [ new TaxonomySubtree(42), new ContentTypeIdentifier('article'), ] ); /** @var \Ibexa\Contracts\Core\Repository\SearchService $searchService */ $results = $searchService->findContent($query); ``` The criteria limit the results to content that match all of the conditions listed below: - content is assigned to taxonomy entry `42` or any of its descendants - content type is `article` # UserEmail Criterion > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). UserEmail Search Criterion The [`UserEmail` Search Criterion](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-Core-Repository-Values-Content-Query-Criterion-UserEmail.html) searches for content based on the email assigned to the user account. ## Arguments - `value` - string(s) representing the User email(s) - (optional) `operator` - operator constant (IN, EQ, LIKE) ## Limitations Solr search engine and Elasticsearch support IN and EQ operators only. ## Example ### PHP ```php use Ibexa\Contracts\Core\Repository\Values\Content\Query; use Ibexa\Contracts\Core\Repository\Values\Content\Query\Criterion; $query = new Query(); $query->query = new Criterion\UserEmail(['johndoe']); ``` ```php use Ibexa\Contracts\Core\Repository\Values\Content\Query; use Ibexa\Contracts\Core\Repository\Values\Content\Query\Criterion; $query = new Query(); $query->query = new Criterion\UserEmail('nospam*', Criterion\Operator::LIKE); ``` ### REST API **XML** ```xml j.black* ``` **JSON** ```json "Query": { "Filter": { "UserEmailCriterion": "j.black*" } } ``` # UserId Criterion > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). UserId Search Criterion The [`UserId` Search Criterion](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-Core-Repository-Values-Content-Query-Criterion-UserId.html) searches for content based on the User ID. ## Arguments - `value` - int(s) representing the User ID(s) ## Example ### PHP ```php use Ibexa\Contracts\Core\Repository\Values\Content\Query; use Ibexa\Contracts\Core\Repository\Values\Content\Query\Criterion; $query = new Query(); $query->query = new Criterion\UserId([14]); ``` ### REST API **XML** ```xml 14 ``` **JSON** ```json "Query": { "Filter": { "UserIdCriterion": "14" } } ``` # UserLogin Criterion > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). UserLogin Search Criterion The [`UserLogin` Search Criterion](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-Core-Repository-Values-Content-Query-Criterion-UserLogin.html) searches for content based on the User ID. ## Arguments - `value` - string(s) representing the User logins(s) - (optional) `operator` - operator constant (IN, EQ, LIKE) ## Limitations Solr search engine and Elasticsearch support IN and EQ operators only. ## Example ### PHP ```php use Ibexa\Contracts\Core\Repository\Values\Content\Query; use Ibexa\Contracts\Core\Repository\Values\Content\Query\Criterion; $query = new Query(); $query->query = new Criterion\UserLogin(['johndoe']); ``` ```php use Ibexa\Contracts\Core\Repository\Values\Content\Query; use Ibexa\Contracts\Core\Repository\Values\Content\Query\Criterion; $query = new Query(); $query->query = new Criterion\UserLogin('adm*', Criterion\Operator::LIKE); ``` ### REST API **XML** ```xml johndoe ``` **JSON** ```json "Query": { "Filter": { "UserLoginCriterion": "johndoe" } } ``` # UserMetadata Criterion > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). UserMetadata Search Criterion The [`UserMetadata` Search Criterion](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-Core-Repository-Values-Content-Query-Criterion-UserMetadata.html) searches for content based on its creator or modifier. ## Arguments - `target` - UserMetadata constant (OWNER, GROUP, MODIFIER); GROUP means the user group of the content item's creator - `operator` - Operator constant (EQ, IN) - `value` - int(s) representing the User IDs or user group IDs (in case of the UserMetadata::GROUP target) ## Example ### PHP ```php use Ibexa\Contracts\Core\Repository\Values\Content\Query; use Ibexa\Contracts\Core\Repository\Values\Content\Query\Criterion; $query = new Query(); $query->query = new Criterion\UserMetadata(Criterion\UserMetadata::GROUP, Criterion\Operator::EQ, 12); ``` ### REST API **XML** ```xml GROUP EQ 12 ``` **JSON** ```json { "Query": { "Filter": { "UserMetadataCriterion": { "target": "GROUP", "operator": "EQ", "value": 12 } } } } ``` ## Use case You can use the `UserMetadata` Criterion to search for blog posts created by the Contributor user group: ```php use Ibexa\Contracts\Core\Repository\Values\Content\LocationQuery; use Ibexa\Contracts\Core\Repository\Values\Content\Query\Criterion; // ID of your custom Contributor User Group $contributorGroupId = 32; $query = new LocationQuery(); $query->query = new Criterion\LogicalAnd( [ new Criterion\ContentTypeIdentifier('blog_post'), new Criterion\UserMetadata(Criterion\UserMetadata::GROUP, Criterion\Operator::EQ, $contributorGroupId), ] ); ``` # Visibility Criterion > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). Visibility Search Criterion The [`Visibility` Search Criterion](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-Core-Repository-Values-Content-Query-Criterion-Visibility.html) searches for content based on whether it's visible or not. This Criterion takes into account both hiding content and hiding locations. When used with Content Search, the Criterion takes into account all assigned locations. This means that hidden content is returned if it has at least one visible location. Use Location Search to avoid this. ## Arguments - `value` - Visibility constant (VISIBLE, HIDDEN) ## Example ### PHP ```php use Ibexa\Contracts\Core\Repository\Values\Content\Query; use Ibexa\Contracts\Core\Repository\Values\Content\Query\Criterion; $query = new Query(); $query->query = new Criterion\Visibility(Criterion\Visibility::HIDDEN); ``` ### REST API **XML** ```xml HIDDEN ``` **JSON** ```json "Query": { "Filter": { "VisibilityCriterion": "HIDDEN" } } ``` # LogicalAnd Criterion > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). LogicalAnd Search Criterion The [`LogicalAnd` Search Criterion](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-Core-Repository-Values-Content-Query-Criterion-LogicalAnd.html) matches content if all provided Criteria match. When querying for [products](https://doc.ibexa.co/en/saas/product_catalog/product_api/index.md), use [LogicalAnd](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-ProductCatalog-Values-Product-Query-Criterion-LogicalAnd.html) instead. ## Arguments - `criterion` - a set of Criteria combined by the logical operator ## Example ### PHP ```php use Ibexa\Contracts\Core\Repository\Values\Content\Query; use Ibexa\Contracts\Core\Repository\Values\Content\Query\Criterion; $query = new Query(); $query->query = new Criterion\LogicalAnd( [ new Criterion\ContentTypeIdentifier('article'), new Criterion\SectionIdentifier(['sports', 'news']), ] ); ``` ### REST API **XML** ```xml article news ``` **JSON** ```json { "Query": { "Filter": { "AND": { "ContentTypeIdentifierCriterion": "article", "SectionIdentifierCriterion": "news" } } } } ``` # LogicalNot Criterion > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). LogicalNot Search Criterion The [`LogicalNot` Search Criterion](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-Core-Repository-Values-Content-Query-Criterion-LogicalNot.html) matches content URL if the provided Criterion doesn't match. It takes only one Criterion in the array parameter. ## Arguments - `criterion` - represents the Criterion that should be negated ## Example ```php use Ibexa\Contracts\Core\Repository\Values\Content\Query; use Ibexa\Contracts\Core\Repository\Values\Content\Query\Criterion; $contentTypeIdentifier = 'article'; $query = new Query(); $query->filter = new Criterion\LogicalNot( new Criterion\ContentTypeIdentifier($contentTypeIdentifier) ); ``` ### REST API **XML** ```xml article ``` **JSON** ```json { "Query": { "Criterion": { "LogicalNotCriterion": { "ContentTypeIdentifierCriterion": "article" } } } } ``` # LogicalOr Criterion > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). LogicalOr Search Criterion The [`LogicalOr` Search Criterion](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-Core-Repository-Values-Content-Query-Criterion-LogicalOr.html) matches content if at least one of the provided Criteria matches. When querying for [products](https://doc.ibexa.co/en/saas/product_catalog/product_api/index.md), use [LogicalOr](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-ProductCatalog-Values-Product-Query-Criterion-LogicalOr.html) instead. ## Arguments - `criterion` - a set of Criteria combined by the logical operator ## Example ### PHP ```php use Ibexa\Contracts\Core\Repository\Values\Content\Query; use Ibexa\Contracts\Core\Repository\Values\Content\Query\Criterion; $query = new Query(); $query->filter = new Criterion\LogicalOr( [ new Criterion\ContentTypeIdentifier('article'), new Criterion\SectionIdentifier(['sports', 'news']), ] ); ``` ### REST API **XML** ```xml article news ``` **JSON** ```json { "Query": { "Filter": { "OR": { "ContentTypeIdentifierCriterion": "article", "SectionIdentifierCriterion": "news" } } } } ``` # Content Type Search Criteria reference > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). Content Type Search Criteria help define and fine-tune search queries for content types. Content Type Search Criteria are only supported by [Content Type Search (`ContentTypeService::findContentTypes`)](https://doc.ibexa.co/en/saas/content_management/content_api/managing_content/#finding-and-filtering-content-types). | Criterion | Description | | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------- | | [ContainsFieldDefinitionId](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-Core-Repository-Values-ContentType-Query-Criterion-ContainsFieldDefinitionId.html) | Matches content types that contain a field definition with the specified ID. | | [ContentTypeGroupId](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-Core-Repository-Values-ContentType-Query-Criterion-ContentTypeGroupId.html) | Matches content types by their assigned group ID. | | [ContentTypeGroupName](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-Core-Repository-Values-ContentType-Query-Criterion-ContentTypeGroupName.html) | Matches content types by the name of their assigned group. | | [ContentTypeId](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-Core-Repository-Values-ContentType-Query-Criterion-ContentTypeId.html) | Matches content types by their ID. | | [ContentTypeIdentifier](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-Core-Repository-Values-ContentType-Query-Criterion-ContentTypeIdentifier.html) | Matches content types by their identifier. | | [IsSystem](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-Core-Repository-Values-ContentType-Query-Criterion-IsSystem.html) | Matches content types based on whether the group they belong to is system or not. | | [LogicalAnd](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-Core-Repository-Values-ContentType-Query-Criterion-LogicalAnd.html) | Implements a logical AND Criterion. It matches if ALL of the provided Criteria match. | | [LogicalOr](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-Core-Repository-Values-ContentType-Query-Criterion-LogicalOr.html) | Implements a logical OR Criterion. It matches if at least one of the provided Criteria matches. | | [LogicalNot](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-Core-Repository-Values-ContentType-Query-Criterion-LogicalNot.html) | Implements a logical NOT Criterion. It matches if the provided Criterion doesn't match. | The following example shows how to use them to search for content types: ```php contentTypeService->findContentTypes($query); $output->writeln('Found ' . $searchResult->getTotalCount() . ' content type(s):'); foreach ($searchResult->getContentTypes() as $contentType) { $output->writeln(sprintf( '- [%d] %s (identifier: %s)', $contentType->id, $contentType->getName(), $contentType->identifier )); } return Command::SUCCESS; } } ``` # Product Search Criteria reference > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). Product Search Criteria Product Search Criteria are supported by [product and product variant search](https://doc.ibexa.co/en/saas/product_catalog/product_api/#products) with the following methods: - `ProductServiceInterface::findProducts()` - `ProductServiceInterface::findProductVariants()` - `ProductServiceInterface::findVariants()` Search Criterion let you filter product by specific attributes, for example, color, availability, or price. ## Product Search Criteria To query for products coming from Quable, see [Quable Search API](https://doc.ibexa.co/en/saas/product_catalog/quable/quable_api/#search-for-products) for details about the integration. | Search Criterion | Search based on | Local product catalog | Quable | | ---------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------- | --------------------- | ------ | | [AttributeGroupIdentifier](https://doc.ibexa.co/en/saas/search/criteria_reference/attributegroupidentifier_criterion/index.md) | Value of product's attribute group identifier | Yes | | | [AttributeName](https://doc.ibexa.co/en/saas/search/criteria_reference/attributename_criterion/index.md) | Value of product's attribute name | Yes | | | [BasePrice](https://doc.ibexa.co/en/saas/search/criteria_reference/baseprice_criterion/index.md) | Product's base price | Yes | | | [CatalogIdentifier](https://doc.ibexa.co/en/saas/search/criteria_reference/catalogidentifier_criterion/index.md) | Catalog's identifier | Yes | | | [CatalogName](https://doc.ibexa.co/en/saas/search/criteria_reference/catalogname_criterion/index.md) | Catalog's name | Yes | | | [CatalogStatus](https://doc.ibexa.co/en/saas/search/criteria_reference/catalogstatus_criterion/index.md) | Catalog's status | Yes | | | [CheckboxAttribute](https://doc.ibexa.co/en/saas/search/criteria_reference/checkboxattribute_criterion/index.md) | Value of product's checkbox attribute | Yes | | | [ColorAttribute](https://doc.ibexa.co/en/saas/search/criteria_reference/colorattribute_criterion/index.md) | Value of product's color attribute | Yes | | | [CreatedAt](https://doc.ibexa.co/en/saas/search/criteria_reference/createdat_criterion/index.md) | Date and time when product was created | Yes | Yes | | [CreatedAtRange](https://doc.ibexa.co/en/saas/search/criteria_reference/createdatrange_criterion/index.md) | Date and time range when product was created | Yes | | | [CustomPrice](https://doc.ibexa.co/en/saas/search/criteria_reference/customprice_criterion/index.md) | Product's custom price | Yes | | | [DateTimeAttribute](https://doc.ibexa.co/en/saas/search/criteria_reference/datetimeattribute_criterion/index.md) | Value of product's date and time attribute | Yes | | | [DateTimeAttributeRange](https://doc.ibexa.co/en/saas/search/criteria_reference/datetimeattributerange_criterion/index.md) | Value of product's date and time attribute and given time range | Yes | | | [FloatAttribute](https://doc.ibexa.co/en/saas/search/criteria_reference/floatattribute_criterion/index.md) | Value of product's float attribute | Yes | | | [FloatAttributeRange](https://doc.ibexa.co/en/saas/search/criteria_reference/floatattributerange_criterion/index.md) | Value of product's float attribute | Yes | | | [IntegerAttribute](https://doc.ibexa.co/en/saas/search/criteria_reference/integerattribute_criterion/index.md) | Value of product's integer attribute | Yes | | | [IntegerAttributeRange](https://doc.ibexa.co/en/saas/search/criteria_reference/integerattributerange_criterion/index.md) | Value of product's integer attribute | Yes | | | [IsVirtual](https://doc.ibexa.co/en/saas/search/criteria_reference/isvirtual_criterion/index.md) | Product type (virtual or physical) | Yes | | | [LogicalAnd](https://doc.ibexa.co/en/saas/search/criteria_reference/logicaland_criterion/index.md) | Composite criterion to group multiple criteria using the AND condition | Yes | Yes | | [LogicalOr](https://doc.ibexa.co/en/saas/search/criteria_reference/logicalor_criterion/index.md) | Composite criterion to group multiple criteria using the OR condition | Yes | | | [MatchAll](https://doc.ibexa.co/en/saas/search/criteria_reference/matchall_criterion/index.md) | All products | Yes | Yes | | [ProductAvailability](https://doc.ibexa.co/en/saas/search/criteria_reference/productavailability_criterion/index.md) | Product's availability | Yes | | | [ProductCategory](https://doc.ibexa.co/en/saas/search/criteria_reference/productcategory_criterion/index.md) | Product category assigned to product | Yes | Yes | | [ProductCategorySubtree](https://doc.ibexa.co/en/saas/search/criteria_reference/productcategorysubtree_criterion/index.md) | Product category subtree assigned to product | Yes | Yes | | [ProductCode](https://doc.ibexa.co/en/saas/search/criteria_reference/productcode_criterion/index.md) | Product's code | Yes | Yes | | [ProductName](https://doc.ibexa.co/en/saas/search/criteria_reference/productname_criterion/index.md) | Product's name | Yes | Yes | | [ProductStock](https://doc.ibexa.co/en/saas/search/criteria_reference/productstock_criterion/index.md) | Product's numerical stock | Yes | | | [ProductStockRange](https://doc.ibexa.co/en/saas/search/criteria_reference/productstockrange_criterion/index.md) | Product's numerical stock | Yes | | | [ProductType](https://doc.ibexa.co/en/saas/search/criteria_reference/producttype_criterion/index.md) | Product type | Yes | Yes | | [RangeMeasurementAttributeMaximum](https://doc.ibexa.co/en/saas/search/criteria_reference/rangemeasurementattributemaximum_criterion/index.md) | Maximum value of product's measurement range attribute | Yes | | | [RangeMeasurementAttributeMinimum](https://doc.ibexa.co/en/saas/search/criteria_reference/rangemeasurementattributeminimum_criterion/index.md) | Minimum value of product's measurement range attribute | Yes | | | [SelectionAttribute](https://doc.ibexa.co/en/saas/search/criteria_reference/selectionattribute_criterion/index.md) | Value of product's selection attribute | Yes | | | [SimpleMeasurementAttribute](https://doc.ibexa.co/en/saas/search/criteria_reference/simplemeasurementattribute_criterion/index.md) | Value of product's single measurement attribute | Yes | | | [SymbolAttribute](https://doc.ibexa.co/en/saas/search/criteria_reference/symbolattribute_criterion/index.md) | Value of product's symbol attribute | Yes | | | [UpdatedAt](https://doc.ibexa.co/en/saas/search/criteria_reference/updated_at_criterion/index.md) | Product modification date | Yes | Yes | | [UpdatedAtRange](https://doc.ibexa.co/en/saas/search/criteria_reference/updated_at_range_criterion/index.md) | Product modification date range | Yes | | # AttributeName Criterion > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). AttributeName Search Criterion The `AttributeName` Search Criterion searches for products by the value of their attribute name. ## Arguments - `value` - string representing the attribute's name ## Example ### REST API **XML** ```xml measure ``` **JSON** ```json { "AttributeQuery": { "Query": { "AttributeNameCriterion": "measure" } } } ``` # AttributeGroupIdentifier Criterion > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). AttributeGroupIdentifier Search Criterion The `AttributeGroupIdentifier` Search Criterion searches for products by the value of their attribute group identifier. ## Arguments - `value` - string representing the attribute's identifier ## Example ### REST API **XML** ```xml attribute_group ``` **JSON** ```json { "AttributeQuery": { "Query": { "AttributeGroupIdentifier": "attribute_group" } } } ``` # BasePrice Criterion > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). BasePrice Search Criterion The [`BasePrice` Search Criterion](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-ProductCatalog-Values-Product-Query-Criterion-BasePrice.html) searches for products by their base price. ## Arguments - `value` - a `Money\Money` object representing the price in a specific currency - (optional) `operator` - Operator constant (EQ, GT, GTE, LT, LTE, default EQ) ## Limitations The `BasePrice` Criterion isn't available in the Legacy Search engine. ## Example ### PHP ```php use Ibexa\Contracts\ProductCatalog\Values\Product\ProductQuery; use Ibexa\Contracts\ProductCatalog\Values\Product\Query\Criterion; $query = new ProductQuery( null, new \Ibexa\Contracts\ProductCatalog\Values\Product\Query\Criterion\BasePrice( \Money\Money::EUR(12900), \Ibexa\Contracts\ProductCatalog\Values\Product\Query\Criterion\Operator::GTE ) ); ``` # CatalogIdentifier Criterion > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). CatalogIdentifier Search Criterion The `CatalogIdentifier` Search Criterion searches for a catalog by the value of its identifier. ## Arguments - `value` - string representing the catalog's identifier ## Example ### REST API **XML** ```xml catalog_1 ``` **JSON** ```json { "CatalogQuery": { "Query": { "CatalogIdentifierCriterion": "catalog_1", } } } ``` # CatalogName Criterion > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). CatalogName Search Criterion The `CatalogName` Search Criterion searches for catalogs by the value of their name. ## Arguments - `value` - string representing the catalog's name ## Example ### REST API **XML** ```xml Furniture ``` **JSON** ```json { "CatalogQuery": { "Query": { "CatalogNameCriterion": "Furniture" } } } ``` # CatalogStatus Criterion > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). CatalogStatus Search Criterion The `CatalogStatus` Search Criterion searches for catalogs by the value of their status. ## Arguments - `value` - string representing the catalog's status ## Example ### REST API **XML** ```xml published ``` **JSON** ```json { "CatalogQuery": { "Query": { "CatalogStatusCriterion": "published" } } } ``` # CheckboxAttribute Criterion > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). CheckboxAttribute Search Criterion The `CheckboxAttribute` Search Criterion searches for products by the value of their checkbox attribute. ## Arguments - `identifier` - string representing the attribute - `value` - bool representing the attribute value ## Example ### PHP ```php use Ibexa\Contracts\ProductCatalog\Values\Product\ProductQuery; use Ibexa\Contracts\ProductCatalog\Values\Product\Query\Criterion; $query = new ProductQuery( null, new \Ibexa\Contracts\ProductCatalog\Values\Product\Query\Criterion\CheckboxAttribute('automatic', true) ); ``` # ColorAttribute Criterion > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). ColorAttribute Search Criterion The `ColorAttribute` Search Criterion searches for products by the value of their color attribute. ## Arguments - `identifier` - string representing the attribute - `value` - array of strings representing the attribute values ## Example ### PHP ```php use Ibexa\Contracts\ProductCatalog\Values\Product\ProductQuery; use Ibexa\Contracts\ProductCatalog\Values\Product\Query\Criterion; $query = new ProductQuery( null, new \Ibexa\Contracts\ProductCatalog\Values\Product\Query\Criterion\ColorAttribute('color', ['#FF0000']) ); ``` ### REST API **XML** ```xml color #000000 ``` **JSON** ```json { "AttributeQuery": { "Query": { "ColorAttributeCriterion": { "identifier": "color", "value": ["#000000"] }, } } } ``` # CreatedAt Criterion > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). CreatedAt Search Criterion The `CreatedAt` Search Criterion searches for products based on the date when they were created. ## Arguments - `createdAt` (PHP), `created_at` (REST) - indicating the date that should be matched, provided as a `DateTimeInterface` object in PHP, or as a string acceptable by `DateTime` constructor in REST - `operator` - Operator constant (EQ, GT, GTE, LT, LTE) in PHP or its value in REST ## Example ### PHP ```php use Ibexa\Contracts\ProductCatalog\Values\Product\ProductQuery; use Ibexa\Contracts\ProductCatalog\Values\Product\Query\Criterion; $criteria = new \Ibexa\Contracts\ProductCatalog\Values\Product\Query\Criterion\CreatedAt( new DateTime('2023-03-01'), \Ibexa\Contracts\ProductCatalog\Values\Product\Query\Criterion\Operator::GTE, ); $productQuery = new ProductQuery(null, $criteria); ``` ### REST API **XML** ```xml 2023-06-12 >= ``` **JSON** ```json { "ProductQuery": { "Filter": { "CreatedAtCriterion": { "created_at": "2023-06-12", "operator": ">=" } } } } ``` # CreatedAtRange Criterion > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). CreatedAtRange Search Criterion The `CreatedAtRange` Search Criterion searches for products based on the date range when they were created. ## Arguments - `min` - indicating the beginning of the date range, provided as a `DateTimeInterface` object - `max` - indicating the end of the date range, provided as a `DateTimeInterface` object ## Example ### PHP ```php use Ibexa\Contracts\ProductCatalog\Values\Product\ProductQuery; use Ibexa\Contracts\ProductCatalog\Values\Product\Query\Criterion; $criteria = new \Ibexa\Contracts\ProductCatalog\Values\Product\Query\Criterion\CreatedAtRange( new \DateTimeImmutable('2020-07-10T00:00:00+00:00'), new \DateTimeImmutable('2023-07-12T00:00:00+00:00') ); $productQuery = new ProductQuery(null, $criteria); ``` ### REST API **XML** ```xml 2023-06-12 2023-06-20 ``` **JSON** ```json { "ProductQuery": { "Filter": { "CreatedAtRange": { "min": "2023-06-12", "max": "2023-06-20" } } } } ``` # CustomPrice Criterion > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). CustomPrice Search Criterion The `CustomPrice` Search Criterion searches for products by their custom price for a specific customer group. ## Arguments - `value` - a `Money\Money` object representing the price in a specific currency - (optional) `operator` - Operator constant (EQ, GT, GTE, LT, LTE, default EQ) - (optional) `customerGroup` - a `CustomerGroupInterface` object representing the customer group to show prices for. If you don't provide a customer group, the query uses the group related to the current user. ## Limitations The `CustomPrice` Criterion isn't available in the Legacy Search engine. ## Example ### PHP ```php use Ibexa\Contracts\ProductCatalog\Values\Product\ProductQuery; use Ibexa\Contracts\ProductCatalog\Values\Product\Query\Criterion; /** @var \Ibexa\Contracts\ProductCatalog\Values\CustomerGroupInterface $customerGroup */ $query = new ProductQuery( null, new \Ibexa\Contracts\ProductCatalog\Values\Product\Query\Criterion\CustomPrice( \Money\Money::EUR(13800), \Ibexa\Contracts\ProductCatalog\Values\Product\Query\Criterion\Operator::GTE, $customerGroup ) ); ``` # DateTimeAttribute criterion > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). DateTimeAttribute Criterion The [`DateTimeAttribute Search Criterion`](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-ProductCatalogDateTimeAttribute-Search-Criterion-DateTimeAttribute.html) searches for products by value of a specified attribute, based on the [date and time attribute](https://doc.ibexa.co/en/saas/product_catalog/attributes/date_and_time/index.md) type. ## Arguments - `identifier` - attribute's identifier (string) - `value` - searched value ([DateTimeImmutable](https://www.php.net/manual/en/class.datetimeimmutable.php)) ## Operators The following operators are supported: - [FieldValueCriterion::COMPARISON_EQ](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-CoreSearch-Values-Query-Criterion-FieldValueCriterion.html#constant_COMPARISON_EQ) - [FieldValueCriterion::COMPARISON_NEQ](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-CoreSearch-Values-Query-Criterion-FieldValueCriterion.html#constant_COMPARISON_NEQ) - [FieldValueCriterion::COMPARISON_LT](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-CoreSearch-Values-Query-Criterion-FieldValueCriterion.html#constant_COMPARISON_LT) - [FieldValueCriterion::COMPARISON_LTE](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-CoreSearch-Values-Query-Criterion-FieldValueCriterion.html#constant_COMPARISON_LTE) - [FieldValueCriterion::COMPARISON_GT](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-CoreSearch-Values-Query-Criterion-FieldValueCriterion.html#constant_COMPARISON_GT) - [FieldValueCriterion::COMPARISON_GTE](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-CoreSearch-Values-Query-Criterion-FieldValueCriterion.html#constant_COMPARISON_GTE) ## Example ### PHP The following example lists all products for which the `event_date` attribute has value equal to 2025-07-06. ```php setOperator(FieldValueCriterion::COMPARISON_EQ); $query->setFilter($filter); /** @var \Ibexa\Contracts\ProductCatalog\ProductServiceInterface $productService */ $results = $productService->findProducts($query); ``` # DateTimeAttributeRange criterion > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). DateTimeAttributeRange Criterion The [`DateTimeAttributeRange Search Criterion`](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-ProductCatalogDateTimeAttribute-Search-Criterion-DateTimeAttributeRange.html) searches for products by value of a specified attribute, which must be based on the [date and time attribute](https://doc.ibexa.co/en/saas/product_catalog/attributes/date_and_time/index.md) type. ## Arguments - `identifier` - attribute's identifier (string) - `min` - lower range value (inclusive) of [DateTimeImmutable](https://www.php.net/manual/en/class.datetimeimmutable.php) type. Optional. - `max` - upper range value (inclusive) of [DateTimeImmutable](https://www.php.net/manual/en/class.datetimeimmutable.php) type. Optional. ## Example ### PHP The following example lists all products for which the `event_date` attribute has value greater than 2025-01-01. ```php setFilter(new DateTimeAttributeRange('event_date', new DateTimeImmutable('2025-01-01'))); /** @var \Ibexa\Contracts\ProductCatalog\ProductServiceInterface $productService */ $results = $productService->findProducts($query); ``` # FloatAttribute Criterion > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). FloatAttribute Search Criterion The `FloatAttribute` Search Criterion searches for products by the value of their float attribute. ## Arguments - `identifier` - string representing the attribute - `value` - string representing the attribute value ## Example ### PHP ```php use Ibexa\Contracts\ProductCatalog\Values\Product\ProductQuery; use Ibexa\Contracts\ProductCatalog\Values\Product\Query\Criterion; $query = new ProductQuery( null, new \Ibexa\Contracts\ProductCatalog\Values\Product\Query\Criterion\FloatAttribute( 'length', 16.5 ) ); ``` ### REST API **XML** ```xml length 16.5 ``` **JSON** ```json { "AttributeQuery": { "Query": { "FloatAttributeCriterion": { "identifier": "length", "value": 16.5 } } } } ``` # FloatAttributeRange Criterion > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). FloatAttributeRange Search Criterion The `FloatAttributeRange` Search Criterion searches for products by the range of values of their float attribute. ## Arguments - `identifier` - string representing the attribute - `min` - indicating the beginning of the range - `max` - indicating the end of the date range ## Example ### REST API **XML** ```xml length 16.5 25 ``` **JSON** ```json { "AttributeQuery": { "Query": { "FloatAttributeRangeCriterion": { "identifier": "length", "min": 16.5, "max": 25 } } } } ``` # IntegerAttribute Criterion > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). IntegerAttribute Search Criterion The `IntegerAttribute` Search Criterion searches for products by the value of their integer attribute. ## Arguments - `identifier` - string representing the attribute - `value` - string representing the attribute value ## Example ### PHP ```php use Ibexa\Contracts\ProductCatalog\Values\Product\ProductQuery; use Ibexa\Contracts\ProductCatalog\Values\Product\Query\Criterion; $query = new ProductQuery( null, new \Ibexa\Contracts\ProductCatalog\Values\Product\Query\Criterion\IntegerAttribute( 'size', 38 ) ); ``` ### REST API **XML** ```xml size 38 ``` **JSON** ```json { "AttributeQuery": { "Query": { "IntegerAttributeCriterion": { "identifier": "size", "value": 38 } } } } ``` # IntegerAttributeRange Criterion > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). IntegerAttributeRange Search Criterion The `IntegerAttributeRange` Search Criterion searches for products by the range of values of their integer attribute. ## Arguments - `identifier` - string representing the attribute - `min` - indicating the beginning of the range - `max` - indicating the end of the date range ## Example ### REST API **XML** ```xml length 16 25 ``` **JSON** ```json { "AttributeQuery": { "Query": { "IntegerAttributeRangeCriterion": { "identifier": "length", "min": 16, "max": 25 } } } } ``` # IsVirtual Criterion > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). IsVirtual Search Criterion The `IsVirtual` Search Criterion searches for virtual or physical products. ## Arguments - (optional) `isVirtual` - bool representing whether to search for virtual (default `true`) or physical (`false`) products. ## Example ### PHP ```php use Ibexa\Contracts\ProductCatalog\Values\Product\ProductQuery; use Ibexa\Contracts\ProductCatalog\Values\Product\Query\Criterion; $query = new ProductQuery( null, new \Ibexa\Contracts\ProductCatalog\Values\Product\Query\Criterion\IsVirtual(true) ); ``` ### REST API **XML** ```xml true ``` **JSON** ```json "ProductQuery": { "Filter": { "IsVirtualCriterion": true } } ``` # ProductAvailability Criterion > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). ProductAvailability Search Criterion The `ProductAvailability` Search Criterion searches for products by the availability flag, the boolean value set per product or variant. To search for products that can be ordered, recreate the availability conditions with [existing product search criteria](https://doc.ibexa.co/en/saas/search/criteria_reference/product_search_criteria/index.md), for example [LogicalAnd](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-ProductCatalog-Values-Product-Query-Criterion-LogicalAnd.html), [LogicalOr](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-ProductCatalog-Values-Product-Query-Criterion-LogicalOr.html), and [`ProductStock`](https://doc.ibexa.co/en/saas/search/criteria_reference/productstock_criterion/index.md). To recreate complex [custom availability strategies](https://doc.ibexa.co/en/saas/product_catalog/create_custom_availability_strategy/index.md), you might need to implement [custom search criteria](https://doc.ibexa.co/en/saas/search/search_criteria_and_sort_clauses/#custom-criteria-and-sort-clauses) for the conditions not covered by the built-in ones. For more information, see [Availability and computed availability](https://doc.ibexa.co/en/saas/product_catalog/products/#availability-and-computed-availability). ## Arguments - (optional) `productAvailability` - bool representing whether the product is available (default `true`) ## Example ### PHP ```php use Ibexa\Contracts\ProductCatalog\Values\Product\ProductQuery; use Ibexa\Contracts\ProductCatalog\Values\Product\Query\Criterion; $query = new ProductQuery( null, new \Ibexa\Contracts\ProductCatalog\Values\Product\Query\Criterion\ProductAvailability(true) ); ``` ### REST API **XML** ```xml false ``` **JSON** ```json { "ProductQuery": { "Filter": { "ProductAvailabilityCriterion": false } } } ``` # ProductStock Criterion > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). ProductStock Search Criterion The `ProductStock` Search Criterion searches for products by their numerical stock. ## Arguments - `value` - the numerical stock to search for - (optional) `operator` - operator string (`=` `<` `<=` `>` `>=`) ## Example ### PHP ```php use Ibexa\Contracts\ProductCatalog\Values\Product\ProductQuery; use Ibexa\Contracts\ProductCatalog\Values\Product\Query\Criterion; $productQuery = new ProductQuery( null, new Criterion\ProductStock(10) ); ``` ```php use Ibexa\Contracts\ProductCatalog\Values\Product\ProductQuery; use Ibexa\Contracts\ProductCatalog\Values\Product\Query\Criterion; $productQuery = new ProductQuery( null, new Criterion\ProductStock(50, '>=') ); ``` # ProductStockRange Criterion > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). ProductStockRange Search Criterion The `ProductStockRange` Search Criterion searches for products by their numerical stock. ## Arguments - `min` - minimum stock - `max` - maximum stock ## Example ### PHP ```php use Ibexa\Contracts\ProductCatalog\Values\Product\ProductQuery; use Ibexa\Contracts\ProductCatalog\Values\Product\Query\Criterion; $productQuery = new ProductQuery( null, new Criterion\ProductStockRange(10, 120) ); ``` # ProductCategory Criterion > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). ProductCategory Search Criterion The `ProductCategory` Search Criterion searches for products by the category they're assigned to. ## Arguments - `taxonomyEntries` - array of ints representing category IDs ## Example ### PHP ```php use Ibexa\Contracts\ProductCatalog\Values\Product\ProductQuery; use Ibexa\Contracts\ProductCatalog\Values\Product\Query\Criterion; $query = new ProductQuery( null, new \Ibexa\Contracts\ProductCatalog\Values\Product\Query\Criterion\ProductCategory([2, 3]) ); ``` ### REST API **XML** ```xml [2, 3] ``` **JSON** ```json { "ProductQuery": { "Filter": { "ProductCategoryCriterion": [ 2, 3 ] } } } ``` # ProductCategorySubtree Criterion > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). ProductCategorySubtree Search Criterion The `ProductCategorySubtree` Search Criterion searches for products assigned to a given product category or any of its subcategories. Unlike the [`ProductCategory` criterion](https://doc.ibexa.co/en/saas/search/criteria_reference/productcategory_criterion/index.md), which matches products assigned to specific category IDs, `ProductCategorySubtree` matches the entire subtree rooted at the provided category, including all descendant categories. ## Arguments - `taxonomyEntryId` - int representing the ID of the root taxonomy entry (product category) of the subtree to search within ## Example ### PHP ```php setQuery($criteria); $results = $productService->findProducts($productQuery); ``` # ProductCode Criterion > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). ProductCode Search Criterion The `ProductCode` Search Criterion searches for products by their codes. ## Arguments - `productCode` - array of strings representing the product codes(s) ## Example ### PHP ```php use Ibexa\Contracts\ProductCatalog\Values\Product\ProductQuery; use Ibexa\Contracts\ProductCatalog\Values\Product\Query\Criterion; $query = new ProductQuery( null, new \Ibexa\Contracts\ProductCatalog\Values\Product\Query\Criterion\ProductCode(['ergo_desk', 'alter_desk']) ); ``` ### REST API **XML** ```xml ski snowboard ``` **JSON** ```json { "ProductQuery": { "Filter": { "ProductCodeCriterion": [ "ski", "snowboard" ] } } } ``` # ProductName Criterion > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). ProductName Search Criterion The `ProductName` Search Criterion searches for products by their names. ## Arguments - `productName` - string representing the Product name, with `*` as wildcard ## Example ### PHP ```php use Ibexa\Contracts\ProductCatalog\Values\Product\ProductQuery; use Ibexa\Contracts\ProductCatalog\Values\Product\Query\Criterion; $query = new ProductQuery( null, new \Ibexa\Contracts\ProductCatalog\Values\Product\Query\Criterion\ProductName('sofa*') ); ``` ### REST API **XML** ```xml sofa* ``` **JSON** ```json { "ProductQuery": { "Filter": { "ProductNameCriterion": "sofa*" } } } ``` # ProductType Criterion > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). ProductType Search Criterion The `ProductType` Search Criterion searches for products by their codes. ## Arguments - `productType` - array of strings representing the product type(s) ## Example ### PHP ```php use Ibexa\Contracts\ProductCatalog\Values\Product\ProductQuery; use Ibexa\Contracts\ProductCatalog\Values\Product\Query\Criterion; $query = new ProductQuery( null, new \Ibexa\Contracts\ProductCatalog\Values\Product\Query\Criterion\ProductType(['dress']) ); ``` ### REST API **XML** ```xml desk ``` **JSON** ```json { "ProductQuery": { "Filter": { "ProductTypeCriterion": "desk" } } } ``` # RangeMeasurementAttributeMinimum Criterion > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). RangeMeasurementAttributeMinimum Search Criterion The `RangeMeasurementAttributeMinimum` Search Criterion searches for products by the minimum value of their measurement (range) attribute. ## Arguments - `identifier` - string representing the attribute - `value` - `\Ibexa\Contracts\Measurement\Value\SimpleValueInterface` object representing the minimum attribute value ## Example ### PHP ```php use Ibexa\Contracts\ProductCatalog\Values\Product\ProductQuery; use Ibexa\Contracts\ProductCatalog\Values\Product\Query\Criterion; /** @var \Ibexa\Contracts\Measurement\MeasurementServiceInterface $measurementService */ $value = $measurementService->buildSimpleValue('length', 100, 'centimeter'); $query = new ProductQuery( null, new \Ibexa\Contracts\Measurement\Product\Query\Criterion\RangeMeasurementAttributeMinimum( 'length', $value ) ); ``` # RangeMeasurementAttributeMaximum Criterion > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). RangeMeasurementAttributeMaximum Search Criterion The `RangeMeasurementAttributeMaximum` Search Criterion searches for products by the maximum value of their measurement (range) attribute. ## Arguments - `identifier` - string representing the attribute - `value` - `\Ibexa\Contracts\Measurement\Value\SimpleValueInterface` object representing the maximum attribute value ## Example ### PHP ```php use Ibexa\Contracts\ProductCatalog\Values\Product\ProductQuery; use Ibexa\Contracts\ProductCatalog\Values\Product\Query\Criterion; /** @var \Ibexa\Contracts\Measurement\MeasurementServiceInterface $measurementService */ $value = $measurementService->buildSimpleValue('length', 150, 'centimeter'); $query = new ProductQuery( null, new \Ibexa\Contracts\Measurement\Product\Query\Criterion\RangeMeasurementAttributeMaximum( 'length', $value ) ); ``` # SimpleMeasurementAttribute Criterion > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). SimpleMeasurementAttribute Search Criterion The `SimpleMeasurementAttribute` Search Criterion searches for products by the value of their measurement (single) attribute. ## Arguments - `identifier` - string representing the attribute - `value` - `Ibexa\Contracts\Measurement\Value\SimpleValueInterface` object representing the attribute value ## Example ### PHP ```php use Ibexa\Contracts\ProductCatalog\Values\Product\ProductQuery; use Ibexa\Contracts\ProductCatalog\Values\Product\Query\Criterion; /** @var \Ibexa\Contracts\Measurement\MeasurementServiceInterface $measurementService */ $value = $measurementService->buildSimpleValue('length', 120, 'centimeter'); $query = new ProductQuery( null, new \Ibexa\Contracts\Measurement\Product\Query\Criterion\SimpleMeasurementAttribute( 'width', $value ) ); ``` # SelectionAttribute Criterion > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). SelectionAttribute Search Criterion The `SelectionAttribute` Search Criterion searches for products by the value of their selection attribute. ## Arguments - `identifier` - string representing the attribute - `value` - array of strings representing the attribute values ## Example ### PHP ```php use Ibexa\Contracts\ProductCatalog\Values\Product\ProductQuery; use Ibexa\Contracts\ProductCatalog\Values\Product\Query\Criterion; $query = new ProductQuery( null, new \Ibexa\Contracts\ProductCatalog\Values\Product\Query\Criterion\SelectionAttribute( 'fabric_type', ['cotton'] ) ); ``` ### REST API **XML** ```xml fabric_type [cotton] ``` **JSON** ```json { "AttributeQuery": { "Query": { "SelectionAttributeCriterion": { "identifier": "fabric_type", "value": [ "cotton" ] } } } } ``` # SymbolAttributeCriterion > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). SymbolAttribute Criterion The `SymbolAttribute` Search Criterion searches for products by [symbol attribute](https://doc.ibexa.co/en/saas/product_catalog/attributes/symbol_attribute_type/index.md). ## Arguments - `identifier` - identifier of the format - `value` - array with the values to search for ## Example ### PHP ```php setFilter(new SymbolAttribute('ean', ['5023920187205'])); /** @var \Ibexa\Contracts\ProductCatalog\ProductServiceInterface $productService */ $results = $productService->findProducts($query); ``` # UpdatedAt Criterion > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). UpdatedAt Search Criterion The `UpdatedAt` Search Criterion searches for products based on the date when they were last updated. ## Arguments - `date` - indicating the date that should be matched, provided as a [`DateTimeInterface`](https://www.php.net/manual/en/class.datetimeinterface.php) object in PHP, or as a string acceptable by `DateTimeInterface` constructor in REST - `operator` - Operator constant (EQ, GT, GTE, LT, LTE) in PHP or its value in REST ## Operators | Operator | Value | Description | | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ----- | ------------------------------------------------------------ | | [`Operator::EQ`](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-ProductCatalog-Values-Product-Query-Criterion-Operator.html#constant_EQ) | `=` | Matches products updated exactly on the given date (default) | | [`Operator::GT`](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-ProductCatalog-Values-Product-Query-Criterion-Operator.html#constant_GT) | `>` | Matches products updated after the given date | | [`Operator::GTE`](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-ProductCatalog-Values-Product-Query-Criterion-Operator.html#constant_GTE) | `>=` | Matches products updated on or after the given date | | [`Operator::LT`](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-ProductCatalog-Values-Product-Query-Criterion-Operator.html#constant_LT) | `<` | Matches products updated before the given date | | [`Operator::LTE`](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-ProductCatalog-Values-Product-Query-Criterion-Operator.html#constant_LTE) | `<=` | Matches products updated on or before the given date | ## Example ### PHP ```php setQuery($criteria); $results = $productService->findProducts($productQuery); ``` ### REST API **XML** ```xml 2023-06-12 >= ``` **JSON** ```json { "ProductQuery": { "Filter": { "UpdatedAtCriterion": { "updated_at": "2023-06-12", "operator": ">=" } } } } ``` # UpdatedAtRange Criterion > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). UpdatedAtRange Search Criterion The `UpdatedAtRange` Search Criterion searches for products based on the date range when they were last updated. ## Arguments - `min` - the start of the date range (inclusive), provided as a [`DateTimeInterface`](https://www.php.net/manual/en/class.datetimeinterface.php) object in PHP, or as a string acceptable by `DateTimeInterface` constructor in REST - `max` - the end of the date range (inclusive), provided as a [`DateTimeInterface`](https://www.php.net/manual/en/class.datetimeinterface.php) object in PHP, or as a string acceptable by `DateTimeInterface` constructor in REST At least one of `min` or `max` must be provided. ## Example ### PHP ```php setQuery($criteria); $results = $productService->findProducts($productQuery); ``` ### REST API **XML** ```xml 2023-06-12 2023-06-20 ``` **JSON** ```json { "ProductQuery": { "Filter": { "UpdatedAtRangeCriterion": { "min": "2023-06-12", "max": "2023-06-20" } } } } ``` # Price Search Criteria reference > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). Price Search Criteria Price Search Criteria are only supported by [Price Search (`ProductPriceServiceInterface::findPrices`)](https://doc.ibexa.co/en/saas/product_catalog/price_api/#prices). With these Criteria you can filter prices by currency, customer group, product, and more. ## Price Search Criteria | Search Criterion | Search based on | | -------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------- | | [Currency](https://doc.ibexa.co/en/saas/search/criteria_reference/price_currency_criterion/index.md) | Currency | | [CustomerGroup](https://doc.ibexa.co/en/saas/search/criteria_reference/price_customergroup_criterion/index.md) | A customer group that the price applies to | | [IsBasePrice](https://doc.ibexa.co/en/saas/search/criteria_reference/price_isbaseprice_criterion/index.md) | Boolean that indicates whether the price is a base price | | [IsCustomPrice](https://doc.ibexa.co/en/saas/search/criteria_reference/price_iscustomprice_criterion/index.md) | Boolean that indicates whether the price is a custom price | | [LogicalAnd](https://doc.ibexa.co/en/saas/search/criteria_reference/price_logicaland_criterion/index.md) | Logical AND criterion that matches if all the provided Criteria match | | [LogicalOr](https://doc.ibexa.co/en/saas/search/criteria_reference/price_logicalor_criterion/index.md) | Logical OR criterion that matches if at least one of the provided Criteria matches | | [Product](https://doc.ibexa.co/en/saas/search/criteria_reference/price_product_criterion/index.md) | Product code | # Price Currency Criterion > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). Price Currency Search Criterion The `Currency` Search Criterion searches for prices based on the given currency. ## Arguments - `currency` - a single object or an array of `CurrencyInterface` objects that represent the currency (`Ibexa\Contracts\ProductCatalog\Values\CurrencyInterface`) ## Example ### PHP ```php use Ibexa\Contracts\Core\Repository\Values\Content\Query\Criterion; use Ibexa\Contracts\ProductCatalog\Values\Price\PriceQuery; /** @var \Ibexa\Contracts\ProductCatalog\CurrencyServiceInterface $currencyService */ $currency = $currencyService->getCurrencyByCode('EUR'); $query = new PriceQuery( new \Ibexa\Contracts\ProductCatalog\Values\Price\Query\Criterion\Currency($currency) ); ``` # Price CustomerGroup Criterion > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). Price CustomerGroup Search Criterion The `CustomerGroup` Search Criterion searches for prices based on the customer group. ## Arguments - `customer_group` - a single object or an array or `CustomerGroupInterface` objects that represent the customer group (`Ibexa\Contracts\ProductCatalog\Values\CustomerGroupInterface`) ## Example ### PHP ```php use Ibexa\Contracts\Core\Repository\Values\Content\Query\Criterion; use Ibexa\Contracts\ProductCatalog\Values\Price\PriceQuery; /** @var \Ibexa\Contracts\ProductCatalog\CustomerGroupServiceInterface $customerGroupService */ $customerGroup = $customerGroupService->getCustomerGroup(123); $query = new PriceQuery( new \Ibexa\Contracts\ProductCatalog\Values\Price\Query\Criterion\CustomerGroup($customerGroup) ); ``` # Price IsBasePrice Criterion > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). Price IsBasePrice Search Criterion The `IsBasePrice` Search Criterion searches for prices that are base prices. ## Arguments This Criterion takes no arguments. ## Limitations The `IsBasePrice` Criterion isn't available in Solr or Elasticsearch engines. ## Example ### PHP ```php use Ibexa\Contracts\Core\Repository\Values\Content\Query\Criterion; use Ibexa\Contracts\ProductCatalog\Values\Price\PriceQuery; $query = new PriceQuery( new \Ibexa\Contracts\ProductCatalog\Values\Price\Query\Criterion\IsBasePrice() ); ``` # Price IsCustomPrice Criterion > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). Price IsCustomPrice Search Criterion The `IsCustomPrice` Search Criterion searches for prices that are custom prices. ## Arguments This Criterion takes no arguments. ## Limitations The `IsCustomPrice` Criterion isn't available in Solr or Elasticsearch engines. ## Example ### PHP ```php use Ibexa\Contracts\Core\Repository\Values\Content\Query\Criterion; use Ibexa\Contracts\ProductCatalog\Values\Price\PriceQuery; $query = new PriceQuery( new \Ibexa\Contracts\ProductCatalog\Values\Price\Query\Criterion\IsCustomPrice() ); ``` # Price LogicalAnd Criterion > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). Price LogicalAnd Search Criterion The `LogicalAnd` Search Criterion matches prices if all provided Criteria match. ## Arguments - `criterion` - a set of Criteria combined by the logical operator ## Example ### PHP ```php use Ibexa\Contracts\ProductCatalog\Values\Price\PriceQuery; /** @var \Ibexa\Contracts\ProductCatalog\Values\CurrencyInterface $currencyUSD */ $query = new PriceQuery( new \Ibexa\Contracts\ProductCatalog\Values\Price\Query\Criterion\LogicalAnd( new \Ibexa\Contracts\ProductCatalog\Values\Price\Query\Criterion\Currency($currencyUSD), new \Ibexa\Contracts\ProductCatalog\Values\Price\Query\Criterion\IsCustomPrice() ) ); ``` # Price LogicalOr Criterion > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). Price LogicalOr Search Criterion The `LogicalOr` Search Criterion matches prices if at least one of the provided Criteria matches. ## Arguments - `criterion` - a set of Criteria combined by the logical operator ## Example ### PHP ```php use Ibexa\Contracts\ProductCatalog\Values\Price\PriceQuery; /** * @var \Ibexa\Contracts\ProductCatalog\Values\CurrencyInterface $currencyUSD * @var \Ibexa\Contracts\ProductCatalog\Values\CurrencyInterface $currencyEUR */ $query = new PriceQuery( new \Ibexa\Contracts\ProductCatalog\Values\Price\Query\Criterion\LogicalOr( new \Ibexa\Contracts\ProductCatalog\Values\Price\Query\Criterion\Currency($currencyUSD), new \Ibexa\Contracts\ProductCatalog\Values\Price\Query\Criterion\Currency($currencyEUR) ) ); ``` # Price Product Criterion > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). Price Product Search Criterion The `Product` Search Criterion searches for prices based on product codes. ## Arguments - `product_code` - a string that represents a product code or an array of codes ## Example ### PHP ```php use Ibexa\Contracts\Core\Repository\Values\Content\Query\Criterion; use Ibexa\Contracts\ProductCatalog\Values\Price\PriceQuery; $query = new PriceQuery( new \Ibexa\Contracts\ProductCatalog\Values\Price\Query\Criterion\Product('ergo_desk') ); ``` # URL Search Criteria reference > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). URL Search Criteria help define and fine-tune search queries for URLs. URL Search Criteria are only supported by [URL Search (`URLService::findUrls`)](https://doc.ibexa.co/en/saas/content_management/url_management/url_api/index.md). | URL criteria | URL based on | | ---------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------- | | [LogicalAnd](https://doc.ibexa.co/en/saas/search/url_search_reference/logicaland_url_criterion/index.md) | Implements a logical AND Criterion. It matches if ALL of the provided Criteria match. | | [LogicalNot](https://doc.ibexa.co/en/saas/search/url_search_reference/logicalnot_url_criterion/index.md) | Implements a logical NOT Criterion. It matches if the provided Criterion doesn't match. | | [LogicalOr](https://doc.ibexa.co/en/saas/search/url_search_reference/logicalor_url_criterion/index.md) | Implements a logical OR Criterion. It matches if at least one of the provided Criteria match. | | [MatchAll](https://doc.ibexa.co/en/saas/search/url_search_reference/matchall_url_criterion/index.md) | Returns all URL results. | | [MatchNone](https://doc.ibexa.co/en/saas/search/url_search_reference/matchnone_url_criterion/index.md) | Returns no URL results. | | [Pattern](https://doc.ibexa.co/en/saas/search/url_search_reference/pattern_url_criterion/index.md) | Matches URLs that contain a pattern. | | [SectionId](https://doc.ibexa.co/en/saas/search/url_search_reference/sectionid_url_criterion/index.md) | Matches URLs from content placed in the Section with the specified ID. | | [SectionIdentifier](https://doc.ibexa.co/en/saas/search/url_search_reference/sectionidentifier_url_criterion/index.md) | Matches URLs from content placed in Sections with the specified identifiers. | | [Validity](https://doc.ibexa.co/en/saas/search/url_search_reference/validity_url_criterion/index.md) | Matches URLs based on validity flag. | | [VisibleOnly](https://doc.ibexa.co/en/saas/search/url_search_reference/visibleonly_url_criterion/index.md) | Matches URLs from published content. | # MatchAll Criterion > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). MatchAll Criterion The [`MatchAll` URL Criterion](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-Core-Repository-Values-URL-Query-Criterion-MatchAll.html) is an auxiliary Criterion that returns all search results. It's used internally when no filter or query is provided on a Query object. The Criterion takes no arguments. # MatchNone Criterion > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). MatchNone Criterion The [`MatchNone` URL Criterion](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-Core-Repository-Values-URL-Query-Criterion-MatchNone.html) is an auxiliary Criterion that returns no search results. It's used internally when no filter or query is provided on a Query object. The Criterion takes no arguments. # Pattern Criterion > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). Pattern Criterion The [`Pattern` URL Criterion](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-Core-Repository-Values-URL-Query-Criterion-SectionId.html) matches URLs that contain the provided pattern. ## Arguments - `pattern` - string representing the pattern that needs to be a part of the URL ## Example ```php use Ibexa\Contracts\Core\Repository\Values\URL\Query\Criterion; use Ibexa\Contracts\Core\Repository\Values\URL\URLQuery; $query = new URLQuery(); $query->filter = new Criterion\Pattern('ibexa.co'); ``` # SectionId Criterion > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). SectionId Criterion The [`SectionId` URL Criterion](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-Core-Repository-Values-URL-Query-Criterion-SectionId.html) matches URLs based on the ID of the related content Section. ## Arguments - `sectionIds` - array of ints representing the IDs of the related content Sections ## Example ```php use Ibexa\Contracts\Core\Repository\Values\URL\Query\Criterion; use Ibexa\Contracts\Core\Repository\Values\URL\URLQuery; $query = new URLQuery(); $query->filter = new Criterion\SectionId([1, 3]); ``` # SectionIdentifier Criterion > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). SectionIdentifier Criterion The [SectionIdentifier URL Criterion](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-Core-Repository-Values-URL-Query-Criterion-SectionIdentifier.html) matches URLs related to the content placed in a specified section identifier. ## Arguments - `sectionIdentifiers` - string(s) representing the identifiers of the Section(s) ## Example ```php use Ibexa\Contracts\Core\Repository\Values\URL\Query\Criterion; use Ibexa\Contracts\Core\Repository\Values\URL\URLQuery; $query = new URLQuery(); $query->filter = new Criterion\SectionIdentifier(['standard', 'media']); ``` # Validity Criterion > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). Validity Criterion The [Validity URL Criterion](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-Core-Repository-Values-URL-Query-Criterion-Validity.html) matches URLs based on a validity flag. ## Arguments - `isValid` - bool representing whether the matcher selects only valid URLs ## Example ```php use Ibexa\Contracts\Core\Repository\Values\URL\Query\Criterion; use Ibexa\Contracts\Core\Repository\Values\URL\URLQuery; $query = new URLQuery(); $query->filter = new Criterion\Validity(true); ``` # VisibleOnly Criterion > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). VisibleOnly Criterion The [`VisibleOnly` URL Criterion](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-Core-Repository-Values-URL-Query-Criterion-VisibleOnly.html) matches URLs from the published content. The Criterion takes no arguments. # LogicalAnd Criterion > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). LogicalAnd Criterion The [`LogicalAnd` URL Criterion](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-Core-Repository-Values-URL-Query-Criterion-LogicalAnd.html) matches a URL if all provided Criteria match. ## Arguments - `criterion` - the set of Criteria combined by the logical operator ## Example ```php use Ibexa\Contracts\Core\Repository\Values\URL\Query\Criterion; use Ibexa\Contracts\Core\Repository\Values\URL\URLQuery; $query = new URLQuery(); $query->filter = new Criterion\LogicalAnd( [ new Criterion\Validity(true), new Criterion\Pattern('ibexa.co'), ] ); ``` # LogicalNot Criterion > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). LogicalNot Criterion The [`LogicalNot` URL Criterion](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-Core-Repository-Values-URL-Query-Criterion-LogicalNot.html) matches a URL if the provided Criterion doesn't match. It takes only one Criterion in the array parameter. ## Arguments - `criterion` - represents the Criterion that should be negated ## Example ```php use Ibexa\Contracts\Core\Repository\Values\URL\Query\Criterion; use Ibexa\Contracts\Core\Repository\Values\URL\URLQuery; $query = new URLQuery(); $query->filter = new Criterion\LogicalNot( new Criterion\Pattern('ibexa.co') ); ``` # LogicalOr Criterion > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). LogicalOr Criterion The [`LogicalOr` URL Criterion](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-Core-Repository-Values-URL-Query-Criterion-LogicalOr.html) matches a URL if at least one of the provided Criteria match. ## Arguments - `criterion` - the set of Criteria combined by the logical operator ## Example ```php use Ibexa\Contracts\Core\Repository\Values\URL\Query\Criterion; use Ibexa\Contracts\Core\Repository\Values\URL\URLQuery; $query = new URLQuery(); $query->filter = new Criterion\LogicalOr( [ new Criterion\SectionIdentifier(['sports', 'news']), new Criterion\Pattern('ibexa.co'), ] ); ``` # Activity Log Search Criteria reference > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). Activity Log Search Criteria Activity Log Search Criteria are found in the `Ibexa\Contracts\ActivityLog\Values\ActivityLog\Criterion` namespace. Those Criteria are to be used with `Ibexa\Contracts\ActivityLog\Values\ActivityLog\Query` for `Ibexa\Contracts\ActivityLog\ActivityLogServiceInterface::find`. They're applied to log entry groups. For example, with the criterion `ActionCriterion`, you get log entry groups that have at least one entry with this action (and possibly other actions as well). See [Searching in the Activity Log groups](https://doc.ibexa.co/en/saas/administration/recent_activity/recent_activity/#searching-in-the-activity-log-groups) for how to use a query, and an example combining several criteria. ## Value-based criteria | Search Criterion | Search based on | | ------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------ | | [`ActionCriterion`](https://doc.ibexa.co/en/saas/search/activity_log_search_reference/action_criterion/index.md) | Performed action name(s) | | [`LoggedAtCriterion`](https://doc.ibexa.co/en/saas/search/activity_log_search_reference/logged_at_criterion/index.md) | Before, after or at a given date and time | | [`ObjectCriterion`](https://doc.ibexa.co/en/saas/search/activity_log_search_reference/object_criterion/index.md) | Manipulated object's class name, and optionally objects' IDs | | [`ObjectNameCriterion`](https://doc.ibexa.co/en/saas/search/activity_log_search_reference/object_name_criterion/index.md) | Manipulated object's name, in whole or in part | | [`UserCriterion`](https://doc.ibexa.co/en/saas/search/activity_log_search_reference/user_criterion/index.md) | User performing the action | ## Logical criteria | Search Criterion | Description | | ---------------- | ----------------------------------------------------------------------------------- | | `LogicalNot` | Logical NOT criterion that matches if the provided Criteria don't match. | | `LogicalAnd` | Logical AND criterion that matches if all the provided Criteria match. | | `LogicalOr` | Logical OR criterion that matches if at least one of the provided Criteria matches. | # Action Criterion > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). The `ActionCriterion` Activity Log Criterion matches activity log group that has a log entry with one of the given actions. ## Argument - `actions` - list of action name strings. A set of built-in names is available as `ActivityLogServiceInterface`'s `ACTION_` prefixed constants. ## Example ```php use Ibexa\Contracts\ActivityLog\ActivityLogServiceInterface; use Ibexa\Contracts\ActivityLog\Values\ActivityLog as ActivityLog; $query = new ActivityLog\Query([ new ActivityLog\Criterion\ActionCriterion([ ActivityLogServiceInterface::ACTION_DELETE, ActivityLogServiceInterface::ACTION_TRASH, ]), ]); ``` # LoggedAt Criterion > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). The `LoggedAtCriterion` Activity Log Criterion matches activity log group that has a log entry created before or after a given date time. ## Arguments - `dateTime` - a [`DateTimeInterface`](https://www.php.net/manual/en/class.datetimeinterface.php) object, like [`DateTime`](https://www.php.net/manual/en/class.datetime.php) - `comparison` - string that represents a comparison sign. Available signs can be found as constant in the `LoggedAtCriterion` class itself | Comparison | Value | Constant | | --------------------- | ----- | ------------------------ | | Equal | `=` | `LoggedAtCriterion::EQ` | | Not equal | `<>` | `LoggedAtCriterion::NEQ` | | Less than | `<` | `LoggedAtCriterion::LT` | | Less than or equal | `<=` | `LoggedAtCriterion::LTE` | | Greater than | `>` | `LoggedAtCriterion::GT` | | Greater than or equal | `>=` | `LoggedAtCriterion::GTE` | ## Example The following example is to match all activity log groups that aren't older than a day: ```php use Ibexa\Contracts\ActivityLog\Values\ActivityLog as ActivityLog; $query = new ActivityLog\Query([ new ActivityLog\Criterion\LoggedAtCriterion(new \DateTime('- 1 day'), ActivityLog\Criterion\LoggedAtCriterion::GTE), ]); ``` # Object Criterion > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). The `ObjectCriterion` Activity Log Criterion matches log group with a log entry about the given class name, and eventually one of the given IDs. ## Arguments - `objectClass` - a class of the object concerned by the searched log entries - `ids` - an optional list of object IDs ## Examples ```php use Ibexa\Contracts\ActivityLog\Values\ActivityLog as ActivityLog; $query = new ActivityLog\Query([ new ActivityLog\Criterion\ObjectCriterion(Ibexa\Contracts\Core\Repository\Values\Content\Content::class), ]); ``` ```php use Ibexa\Contracts\ActivityLog\Values\ActivityLog as ActivityLog; $query = new ActivityLog\Query([ new ActivityLog\Criterion\ObjectCriterion(Ibexa\Contracts\ProductCatalog\Values\ProductVariantInterface::class, ['123', '234', '345']), ]); ``` # Object Name Criterion > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). The `ObjectNameCriterion` Activity Log Criterion matches log groups that have a log entry with an object having a given string as name, or part of their name. ## Arguments - `query` - string representing the object name - `operator` - constant representing how to compare log names with the query - `ObjectNameCriterion::OPERATOR_CONTAINS` - `ObjectNameCriterion::OPERATOR_STARTS_WITH` - `ObjectNameCriterion::OPERATOR_ENDS_WITH` - `ObjectNameCriterion::OPERATOR_EQUALS` ## Example ```php use Ibexa\Contracts\ActivityLog\Values\ActivityLog as ActivityLog; $query = new ActivityLog\Query([ new ActivityLog\Criterion\ObjectNameCriterion('Ibexa', ActivityLog\Criterion\ObjectNameCriterion::OPERATOR_CONTAINS), ]); ``` # User Criterion > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). The `UserCriterion` Activity Log Criterion matches log groups that have an activity by one of the users given by their IDs. ## Argument - `ids` - list of user IDs ## Example ```php use Ibexa\Contracts\ActivityLog\Values\ActivityLog as ActivityLog; $query = new ActivityLog\Query([ new ActivityLog\Criterion\UserCriterion([10, 14]), ]); ``` # Action Configuration Search Criterion reference > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). Search Criteria available for Action Configuration search Search criteria are found in the `Ibexa\Contracts\ConnectorAi\ActionConfiguration\Query\Criterion` namespace, implementing the [CriterionInterface](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-ConnectorAi-ActionConfiguration-Query-CriterionInterface.html) interface: | Criterion | Description | | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | [Name](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-ConnectorAi-ActionConfiguration-Query-Criterion-Name.html) | Find Action Configurations matching given name. Use [FieldValueCriterion's constants](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-CoreSearch-Values-Query-Criterion-FieldValueCriterion.html#constants) like `FieldValueCriterion::COMPARISON_CONTAINS` or `FieldValueCriterion::COMPARISON_STARTS_WITH` to specify the matching condition | | [Enabled](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-ConnectorAi-ActionConfiguration-Query-Criterion-Enabled.html) | Find enabled or disabled Action Configurations | | [Identifier](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-ConnectorAi-ActionConfiguration-Query-Criterion-Identifier.html) | Find Action Configuration having the exact given identifier | | [LogicalAnd](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-ConnectorAi-ActionConfiguration-Query-Criterion-LogicalAnd.html) | Composite criterion to group multiple criteria using the AND condition | | [LogicalOr](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-ConnectorAi-ActionConfiguration-Query-Criterion-LogicalOr.html) | Composite criterion to group multiple criteria using the OR condition | | [Type](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-ConnectorAi-ActionConfiguration-Query-Criterion-Type.html) | Find Action Configuration having the exact given type | The following example shows how to use them to find specific Action Configurations: ```php findActionConfigurations($query); ``` The result set contains Action Configurations that are: - enabled, and - with an identifier equal to `casual` or with a name starting with `Casual`. # Notification Search Criteria reference > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). Notification Search Criteria Notification Search Criteria are only supported by Notification Search (`NotificationService::findNotifications`). With these Criteria you can filter notifications by their notification creation date, notification status, and notification type. ## Notification Search Criteria | Search Criterion | Search based on | | ----------------------------------------------------------------------------------------------------------------- | ------------------------------------------- | | [DateCreated](https://doc.ibexa.co/en/saas/search/criteria_reference/notification_datecreated_criterion/index.md) | Date and time when notification was created | | [Status](https://doc.ibexa.co/en/saas/search/criteria_reference/notification_status_criterion/index.md) | Status of the notification | | [Type](https://doc.ibexa.co/en/saas/search/criteria_reference/notification_type_criterion/index.md) | Type of the notification | # Notification DateCreated Criterion > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). Notification DateCreated Search Criterion The `DateCreated` Search Criterion searches for notifications based on the date when they were created. ## Arguments - `created` - date to be matched, provided as a `DateTimeInterface` object - `operator` - optional operator string (GTE, LTE) ## Example ### PHP ```php getRepository(); $notificationService = $repository->getNotificationService(); $query = new NotificationQuery([], 0, 25); $query->addCriterion(new Type('Workflow:Review')); $query->addCriterion(new Status(['unread'])); $from = new \DateTimeImmutable('-7 days'); $to = new \DateTimeImmutable(); $query->addCriterion(new DateCreated($from, $to)); $notificationList = $notificationService->findNotifications($query); ``` # Notification Status Criterion > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). Notification Status Search Criterion The `Status` Search Criterion searches for notifications based on notification status. ## Arguments - `status` - Boolean value that represents the status of the notification ## Example ### PHP ```php getRepository(); $notificationService = $repository->getNotificationService(); $query = new NotificationQuery([], 0, 25); $query->addCriterion(new Type('Workflow:Review')); $query->addCriterion(new Status(['unread'])); $from = new \DateTimeImmutable('-7 days'); $to = new \DateTimeImmutable(); $query->addCriterion(new DateCreated($from, $to)); $notificationList = $notificationService->findNotifications($query); ``` # Type Criterion > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). Type Search Criterion The `Type` Search Criterion searches for notifications by their types. ## Arguments - `type` - string that represents the type of the notification, takes values defined in notification workflow ## Example ### PHP ```php getRepository(); $notificationService = $repository->getNotificationService(); $query = new NotificationQuery([], 0, 25); $query->addCriterion(new Type('Workflow:Review')); $query->addCriterion(new Status(['unread'])); $from = new \DateTimeImmutable('-7 days'); $to = new \DateTimeImmutable(); $query->addCriterion(new DateCreated($from, $to)); $notificationList = $notificationService->findNotifications($query); ``` # Sort Clause reference > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). Sort Clauses help fine-tune sorting order when searching for content and locations. Sort Clauses are the sorting options for Content and Location Search and [Repository filtering](https://doc.ibexa.co/en/saas/search/search_api/#repository-filtering). Capabilities of individual Sort Clauses can depend on the search engine. All Sort Clauses can take the following optional argument: - `sortDirection` - the direction of the sorting, either `Query::SORT_ASC` (default) or `Query::SORT_DESC` ## Sort Clauses | Sort Clause | Sorting based on | Content Search | Location Search | Filtering | Trash | | ----------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------- | -------------- | --------------- | --------- | ----- | | [ContentId](https://doc.ibexa.co/en/saas/search/sort_clause_reference/contentid_sort_clause/index.md) | Content items' ID | Yes | Yes | Yes | | | [ContentName](https://doc.ibexa.co/en/saas/search/sort_clause_reference/contentname_sort_clause/index.md) | Content names | Yes | Yes | Yes | Yes | | [ContentTranslatedName](https://doc.ibexa.co/en/saas/search/sort_clause_reference/contenttranslatedname_sort_clause/index.md) | Translated content names | Yes | Yes | | | | [ContentTypeName](https://doc.ibexa.co/en/saas/search/sort_clause_reference/contenttypename_sort_clause/index.md) | Content items' content type name | | | | Yes | | [CustomField](https://doc.ibexa.co/en/saas/search/sort_clause_reference/customfield_sort_clause/index.md) | Raw search index fields | Yes | Yes | | | | [DateModified](https://doc.ibexa.co/en/saas/search/sort_clause_reference/datemodified_sort_clause/index.md) | The date when content was last modified | Yes | Yes | Yes | | | [DatePublished](https://doc.ibexa.co/en/saas/search/sort_clause_reference/datepublished_sort_clause/index.md) | The date when content was created | Yes | Yes | Yes | | | [DateTrashed](https://doc.ibexa.co/en/saas/search/sort_clause_reference/datetrashed_sort_clause/index.md) | The date when content was sent to trash | | | | Yes | | [Depth](https://doc.ibexa.co/en/saas/search/sort_clause_reference/depth_sort_clause/index.md) | Location depth in the content tree | | Yes | Yes | Yes | | [Field](https://doc.ibexa.co/en/saas/search/sort_clause_reference/field_sort_clause/index.md) | Content of one of content item's fields | Yes | Yes | | | | [Id](https://doc.ibexa.co/en/saas/search/sort_clause_reference/id_sort_clause/index.md) | Location ID | | Yes | Yes | | | [IsMainLocation](https://doc.ibexa.co/en/saas/search/sort_clause_reference/ismainlocation_sort_clause/index.md) | Whether a location is the main location of a content item | | Yes | | | | [MapLocationDistance](https://doc.ibexa.co/en/saas/search/sort_clause_reference/maplocationdistance_sort_clause/index.md) | Distance between the location contained in a MapLocation field and the provided coordinates | Yes | Yes | | | | [Path](https://doc.ibexa.co/en/saas/search/sort_clause_reference/path_sort_clause/index.md) | PathString of the Location | | Yes | Yes | Yes | | [Priority](https://doc.ibexa.co/en/saas/search/sort_clause_reference/priority_sort_clause/index.md) | Location priority | | Yes | Yes | Yes | | [Random](https://doc.ibexa.co/en/saas/search/sort_clause_reference/random_sort_clause/index.md) | Random seed | Yes | Yes | | | | [Score](https://doc.ibexa.co/en/saas/search/sort_clause_reference/score_sort_clause/index.md) | Score of the search result | Yes | Yes | | | | [SectionIdentifier](https://doc.ibexa.co/en/saas/search/sort_clause_reference/sectionidentifier_sort_clause/index.md) | ID of the Section content is assigned to | Yes | Yes | Yes | | | [SectionName](https://doc.ibexa.co/en/saas/search/sort_clause_reference/sectionname_sort_clause/index.md) | Name of the Section content is assigned to | Yes | Yes | Yes | Yes | | [UserLogin](https://doc.ibexa.co/en/saas/search/sort_clause_reference/userlogin_sort_clause/index.md) | Login of the content item's creator | | | | Yes | | [Visibility](https://doc.ibexa.co/en/saas/search/sort_clause_reference/visibility_sort_clause/index.md) | Whether the location is visible or not | | Yes | Yes | | # ContentId Sort Clause > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). ContentId Sort Clause The [`ContentId` Sort Clause](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-Core-Repository-Values-Content-Query-SortClause-ContentId.html) sorts search results by the content items' IDs. ## Arguments - (optional) `sortDirection` - Query or LocationQuery constant, either `Query::SORT_ASC` or `Query::SORT_DESC` ## Example ```php use Ibexa\Contracts\Core\Repository\Values\Content\LocationQuery; use Ibexa\Contracts\Core\Repository\Values\Content\Query\SortClause; $query = new LocationQuery(); $query->sortClauses = [new SortClause\ContentId()]; ``` # ContentName Sort Clause > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). ContentName Sort Clause The [`ContentName` Sort Clause](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-Core-Repository-Values-Content-Query-SortClause-ContentName.html) sorts search results by the content items' names. ## Arguments - (optional) `sortDirection` - Query or LocationQuery constant, either `Query::SORT_ASC` or `Query::SORT_DESC` ## Example ```php use Ibexa\Contracts\Core\Repository\Values\Content\LocationQuery; use Ibexa\Contracts\Core\Repository\Values\Content\Query\SortClause; $query = new LocationQuery(); $query->sortClauses = [new SortClause\ContentName()]; ``` # ContentTranslatedName Sort Clause > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). ContentTranslatedName Sort Clause The [`ContentTranslatedName` Sort Clause](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-Core-Repository-Values-Content-Query-SortClause-ContentTranslatedName.html) sorts search results by the content items' translated names. ## Arguments - (optional) `sortDirection` - Query or LocationQuery constant, either `Query::SORT_ASC` or `Query::SORT_DESC` ## Limitations The `ContentTranslatedName` Sort Clause isn't available in [Repository filtering](https://doc.ibexa.co/en/saas/search/search_api/#repository-filtering). ## Example ```php use Ibexa\Contracts\Core\Repository\Values\Content\LocationQuery; use Ibexa\Contracts\Core\Repository\Values\Content\Query\SortClause; $query = new LocationQuery(); $query->sortClauses = [new SortClause\ContentTranslatedName()]; ``` # ContentTypeName Sort Clause > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). ContentTypeName Sort Clause The [`ContentTypeName` Sort Clause](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-Core-Repository-Values-Content-Query-SortClause-Trash-ContentTypeName.html) sorts the results of searching in Trash by the name of the content item's content type. ## Arguments - (optional) `sortDirection` - Query constant, either `Query::SORT_ASC` or `Query::SORT_DESC` ## Example ```php use Ibexa\Contracts\Core\Repository\Values\Content\Query; use Ibexa\Contracts\Core\Repository\Values\Content\Query\SortClause; $query = new Query(); $query->sortClauses = [new SortClause\Trash\ContentTypeName()]; ``` # CustomField Sort Clause > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). CustomField Sort Clause The [`CustomField` Sort Clause](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-Core-Repository-Values-Content-Query-SortClause-CustomField.html) sorts search results by raw search index fields. ## Arguments - `field` - string representing the search index field name - (optional) `sortDirection` - Query or LocationQuery constant, either `Query::SORT_ASC` or `Query::SORT_DESC` ## Limitations > **Caution: Caution** > > To keep your project search engine independent, don't use the `CustomField` Sort Clause in production code. Valid use cases are: testing, or temporary (one-off) tools. The `CustomField` Sort Clause isn't available in [Repository filtering](https://doc.ibexa.co/en/saas/search/search_api/#repository-filtering). ## Example ```php use Ibexa\Contracts\Core\Repository\Values\Content\LocationQuery; use Ibexa\Contracts\Core\Repository\Values\Content\Query\SortClause; $query = new LocationQuery(); $query->sortClauses = [new SortClause\CustomField('my_custom_field_s')]; ``` # DateModified Sort Clause > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). DateModified Sort Clause The [`DateModified` Sort Clause](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-Core-Repository-Values-Content-Query-SortClause-DateModified.html) sorts search results by the date and time of the last modification of a content item. ## Arguments - (optional) `sortDirection` - Query or LocationQuery constant, either `Query::SORT_ASC` or `Query::SORT_DESC` ## Example ```php use Ibexa\Contracts\Core\Repository\Values\Content\LocationQuery; use Ibexa\Contracts\Core\Repository\Values\Content\Query\SortClause; $query = new LocationQuery(); $query->sortClauses = [new SortClause\DateModified()]; ``` # DatePublished Sort Clause > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). DatePublished Sort Clause The [`DatePublished` Sort Clause](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-Core-Repository-Values-Content-Query-SortClause-DatePublished.html) sorts search results by the date and time of the first publication of a content item. ## Arguments - (optional) `sortDirection` - Query or LocationQuery constant, either `Query::SORT_ASC` or `Query::SORT_DESC` ## Example ```php use Ibexa\Contracts\Core\Repository\Values\Content\LocationQuery; use Ibexa\Contracts\Core\Repository\Values\Content\Query\SortClause; $query = new LocationQuery(); $query->sortClauses = [new SortClause\DatePublished()]; ``` # DateTrashed Sort Clause > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). DateTrashed Sort Clause The [`DateTrashed` Sort Clause](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-Core-Repository-Values-Content-Query-SortClause-Trash-DateTrashed.html) sorts the results of searching in Trash by the date and time when the content item was sent to trash. ## Arguments - (optional) `sortDirection` - Query constant, either `Query::SORT_ASC` or `Query::SORT_DESC` ## Example ```php use Ibexa\Contracts\Core\Repository\Values\Content\Query; use Ibexa\Contracts\Core\Repository\Values\Content\Query\SortClause; $query = new Query(); $query->sortClauses = [new SortClause\Trash\DateTrashed()]; ``` # Depth Sort Clause > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). Depth Sort Clause The [`Location\Depth` Sort Clause](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-Core-Repository-Values-Content-Query-SortClause-Location-Depth.html) sorts search results by the depth of the location in the content tree. ## Arguments - (optional) `sortDirection` - Query or LocationQuery constant, either `Query::SORT_ASC` or `Query::SORT_DESC` ## Example ```php use Ibexa\Contracts\Core\Repository\Values\Content\LocationQuery; use Ibexa\Contracts\Core\Repository\Values\Content\Query\SortClause; $query = new LocationQuery(); $query->sortClauses = [new SortClause\Location\Depth()]; ``` # Field Sort Clause > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). Field Sort Clause The [`Field` Sort Clause](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-Core-Repository-Values-Content-Query-SortClause-Field.html) sorts search results by the value of one of the content items' fields. Search results of the provided content type are sorted in field value order. Results of the query that don't belong to the content type are ranked lower. ## Arguments - `typeIdentifier` - string representing the identifier of the content type to which the field belongs - `fieldIdentifier` - string representing the identifier of the field to sort by - (optional) `sortDirection` - Query or LocationQuery constant, either `Query::SORT_ASC` or `Query::SORT_DESC` ## Limitations The `Field` Sort Clause isn't available in [Repository filtering](https://doc.ibexa.co/en/saas/search/search_api/#repository-filtering). ## Example ```php use Ibexa\Contracts\Core\Repository\Values\Content\LocationQuery; use Ibexa\Contracts\Core\Repository\Values\Content\Query\SortClause; $query = new LocationQuery(); $query->sortClauses = [new SortClause\Field('article', 'title')]; ``` # Id Sort Clause > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). Id Sort Clause The [`Location\Id` Sort Clause](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-Core-Repository-Values-Content-Query-SortClause-Location-Id.html) sorts search results by the ID of the location. ## Arguments - (optional) `sortDirection` - Query or LocationQuery constant, either `Query::SORT_ASC` or `Query::SORT_DESC` ## Example ```php use Ibexa\Contracts\Core\Repository\Values\Content\LocationQuery; use Ibexa\Contracts\Core\Repository\Values\Content\Query\SortClause; $query = new LocationQuery(); $query->sortClauses = [new SortClause\Location\Id()]; ``` # IsMainLocation Sort Clause > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). IsMainLocation Sort Clause The [`Location\IsMainLocation` Sort Clause](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-Core-Repository-Values-Content-Query-SortClause-Location-IsMainLocation.html) sorts search results by whether their location is the main location of the content item. Locations that aren't main locations are ranked as lower values (for example, with ascending order they're returned first). ## Arguments - (optional) `sortDirection` - Query or LocationQuery constant, either `Query::SORT_ASC` or `Query::SORT_DESC` ## Limitations The `Location\IsMainLocation` Sort Clause isn't available in [Repository filtering](https://doc.ibexa.co/en/saas/search/search_api/#repository-filtering). ## Example ```php use Ibexa\Contracts\Core\Repository\Values\Content\LocationQuery; use Ibexa\Contracts\Core\Repository\Values\Content\Query\SortClause; $query = new LocationQuery(); $query->sortClauses = [new SortClause\Location\IsMainLocation()]; ``` # MapLocationDistance Sort Clause > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). MapLocationDistance Sort Clause The [`MapLocationDistance` Sort Clause](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-Core-Repository-Values-Content-Query-SortClause-MapLocationDistance.html) sorts search results by the distance of the indicated MapLocation field to the provided location. ## Arguments - `typeIdentifier` - string representing the identifier of the content type to which the MapLocation field belongs - `fieldIdentifier` - string representing the identifier of the MapLocation field to sort by - `latitude` - float representing the latitude of the location to calculate distance to - `longitude`- float representing the longitude of the location to calculate distance to - (optional) `sortDirection` - Query or LocationQuery constant, either `Query::SORT_ASC` or `Query::SORT_DESC` ## Limitations The `MapLocationDistance` Sort Clause isn't available in [Repository filtering](https://doc.ibexa.co/en/saas/search/search_api/#repository-filtering). ## Example ```php use Ibexa\Contracts\Core\Repository\Values\Content\LocationQuery; use Ibexa\Contracts\Core\Repository\Values\Content\Query\SortClause; $query = new LocationQuery(); $query->sortClauses = [new SortClause\MapLocationDistance('place', 'location', 49.542889, 20.111349)]; ``` # Path Sort Clause > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). Path Sort Clause The [`Location\Path` Sort Clause](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-Core-Repository-Values-Content-Query-SortClause-Location-Path.html) sorts search results by the pathString of the location. > **Note: Note** > > Solr search engine uses dictionary sorting with the `Location/Path` Sort Clause. ## Arguments - (optional) `sortDirection` - Query or LocationQuery constant, either `Query::SORT_ASC` or `Query::SORT_DESC` ## Example ```php use Ibexa\Contracts\Core\Repository\Values\Content\LocationQuery; use Ibexa\Contracts\Core\Repository\Values\Content\Query\SortClause; $query = new LocationQuery(); $query->sortClauses = [new SortClause\Location\Path()]; ``` # Priority Sort Clause > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). Priority Sort Clause The [`Location\Priority` Sort Clause](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-Core-Repository-Values-Content-Query-SortClause-Location-Priority.html) sorts search results by the priority of the location. ## Arguments - (optional) `sortDirection` - Query or LocationQuery constant, either `Query::SORT_ASC` or `Query::SORT_DESC` ## Example ```php use Ibexa\Contracts\Core\Repository\Values\Content\LocationQuery; use Ibexa\Contracts\Core\Repository\Values\Content\Query\SortClause; $query = new LocationQuery(); $query->sortClauses = [new SortClause\Location\Priority()]; ``` # Random Sort Clause > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). Random Sort Clause The [`Random` Sort Clause](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-Core-Repository-Values-Content-Query-SortClause-Random.html) orders search results randomly. ## Arguments - (optional) `seed` - int representing the random seed - (optional) `sortDirection` - Query or LocationQuery constant, either `Query::SORT_ASC` or `Query::SORT_DESC` ## Limitations The `Random` Sort Clause isn't available in [Repository filtering](https://doc.ibexa.co/en/saas/search/search_api/#repository-filtering). In Elasticsearch engine, you cannot combine the `Random` Sort Clause with any other Sort Clause. ## Example ```php use Ibexa\Contracts\Core\Repository\Values\Content\LocationQuery; use Ibexa\Contracts\Core\Repository\Values\Content\Query\SortClause; $query = new LocationQuery(); $query->sortClauses = [new SortClause\Random()]; ``` # Score Sort Clause > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). Score Sort Clause The [`Score` Sort Clause](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-Core-Repository-Values-Content-Query-SortClause-Score.html) orders search results by their score. ## Arguments - (optional) `sortDirection` - Query or LocationQuery constant, either `Query::SORT_ASC` or `Query::SORT_DESC` ## Limitations The `Score` Sort Clause isn't available in [Repository filtering](https://doc.ibexa.co/en/saas/search/search_api/#repository-filtering). ## Example ```php use Ibexa\Contracts\Core\Repository\Values\Content\LocationQuery; use Ibexa\Contracts\Core\Repository\Values\Content\Query\SortClause; $query = new LocationQuery(); $query->sortClauses = [new SortClause\Score()]; ``` # SectionIdentifier Sort Clause > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). SectionIdentifier Sort Clause The [`SectionIdentifier` Sort Clause](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-Core-Repository-Values-Content-Query-SortClause-SectionIdentifier.html) sorts search results by the Section IDs of the content items. ## Arguments - (optional) `sortDirection` - Query or LocationQuery constant, either `Query::SORT_ASC` or `Query::SORT_DESC` > **Note: Note** > > Solr search engine uses the `Query::SORT_DESC` sort direction by default. ## Example ```php use Ibexa\Contracts\Core\Repository\Values\Content\LocationQuery; use Ibexa\Contracts\Core\Repository\Values\Content\Query\SortClause; $query = new LocationQuery(); $query->sortClauses = [new SortClause\SectionIdentifier()]; ``` # SectionName Sort Clause > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). SectionName Sort Clause The [`SectionName` Sort Clause](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-Core-Repository-Values-Content-Query-SortClause-SectionName.html) sorts search results by the Section name of the content items. ## Arguments - (optional) `sortDirection` - Query or LocationQuery constant, either `Query::SORT_ASC` or `Query::SORT_DESC` ## Example ```php use Ibexa\Contracts\Core\Repository\Values\Content\LocationQuery; use Ibexa\Contracts\Core\Repository\Values\Content\Query\SortClause; $query = new LocationQuery(); $query->sortClauses = [new SortClause\SectionName()]; ``` # UserLogin Sort Clause > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). UserLogin Sort Clause The [`UserLogin` Sort Clause](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-Core-Repository-Values-Content-Query-SortClause-Trash-UserLogin.html) sorts the results of searching in Trash by the login of the content item's creator. ## Arguments - (optional) `sortDirection` - Query constant, either `Query::SORT_ASC` or `Query::SORT_DESC` ## Example ```php use Ibexa\Contracts\Core\Repository\Values\Content\Query; use Ibexa\Contracts\Core\Repository\Values\Content\Query\SortClause; $query = new Query(); $query->sortClauses = [new SortClause\Trash\UserLogin()]; ``` # Visibility Sort Clause > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). Visibility Sort Clause The [`Location\Visibility` Sort Clause](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-Core-Repository-Values-Content-Query-SortClause-Location-Visibility.html) sorts search results by whether the location is visible or not. Locations that aren't visible are ranked as higher values (for example, with ascending order they're returned last). ## Arguments - (optional) `sortDirection` - Query or LocationQuery constant, either `Query::SORT_ASC` or `Query::SORT_DESC` ## Example ```php use Ibexa\Contracts\Core\Repository\Values\Content\LocationQuery; use Ibexa\Contracts\Core\Repository\Values\Content\Query\SortClause; $query = new LocationQuery(); $query->sortClauses = [new SortClause\Location\Visibility()]; ``` # Content Type Search Sort Clauses > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). Content Type Search Sort Clauses Content Type Search Sort Clauses are the sorting options for content types. They're only supported by [Content Type Search (`ContentTypeService::findContentTypes`)](https://doc.ibexa.co/en/saas/content_management/content_api/managing_content/#finding-and-filtering-content-types). Sort Clauses are found in the [`Ibexa\Contracts\Core\Repository\Values\ContentType\Query\SortClause`](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/namespaces/ibexa-contracts-core-repository-values-contenttype-query-sortclause.html) namespace: | Name | Description | | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------- | | [Id](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-Core-Repository-Values-ContentType-Query-SortClause-Id.html) | Sort by content type's id | | [Identifier](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-Core-Repository-Values-ContentType-Query-SortClause-Identifier.html) | Sort by content type's identifier | | [Name](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-Core-Repository-Values-ContentType-Query-SortClause-Name.html) | Sort by content type's name | The following example shows how to use them to sort the searched content types: ```php contentTypeService->findContentTypes($query); $output->writeln('Found ' . $searchResult->getTotalCount() . ' content type(s):'); foreach ($searchResult->getContentTypes() as $contentType) { $output->writeln(sprintf( '- [%d] %s (identifier: %s)', $contentType->id, $contentType->getName(), $contentType->identifier )); } return Command::SUCCESS; } } ``` You can change the default sorting order by using the `SORT_ASC` and `SORT_DESC` constants from [`AbstractSortClause`](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-CoreSearch-Values-Query-AbstractSortClause.html#constants). # Product Sort Clauses > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). Product Sort Clauses Product Sort Clauses are only supported by [Product Search (`ProductServiceInterface::findProduct`)](https://doc.ibexa.co/en/saas/product_catalog/product_api/#products). By using Sort Clause you can filter product by specific attributes, for example: price, code, or availability. To sort products coming from Quable, see [Quable Search API](https://doc.ibexa.co/en/saas/product_catalog/quable/quable_api/#search-for-products) for details about the add-on. | Sort Clause | Sorting based on | Local product catalog | Quable | | ------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------ | --------------------- | ------ | | [BasePrice](https://doc.ibexa.co/en/saas/search/sort_clause_reference/baseprice_sort_clause/index.md) | Base product price | Yes | | | [CreatedAt](https://doc.ibexa.co/en/saas/search/sort_clause_reference/createdat_sort_clause/index.md) | Date and time of the creation of a product | Yes | Yes | | [CustomPrice](https://doc.ibexa.co/en/saas/search/sort_clause_reference/customprice_sort_clause/index.md) | Custom product price | Yes | | | [ProductAvailability](https://doc.ibexa.co/en/saas/search/sort_clause_reference/productavailability_sort_clause/index.md) | Product's availability | Yes | | | [ProductCode](https://doc.ibexa.co/en/saas/search/sort_clause_reference/productcode_sort_clause/index.md) | Product's code | Yes | Yes | | [ProductName](https://doc.ibexa.co/en/saas/search/sort_clause_reference/productname_sort_clause/index.md) | Product's name | Yes | Yes | # BasePrice Sort Clause > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). BasePrice Sort Clause The `BasePrice` Sort Clause sorts search results by the product's base price. ## Arguments - `currency` - a `CurrencyInterface` object representing the currency to check price for - (optional) `sortDirection` - ProductQuery constant, either `ProductQuery::SORT_ASC` or `ProductQuery::SORT_DESC` ## Limitations The `BasePrice` Sort Clause isn't available in the Legacy Search engine. ## Example ```php use Ibexa\Contracts\ProductCatalog\Values\CurrencyInterface; use Ibexa\Contracts\ProductCatalog\Values\Product\ProductQuery; /** @var CurrencyInterface $currency */ $sortClauses = [ new \Ibexa\Contracts\ProductCatalog\Values\Product\Query\SortClause\BasePrice( $currency, ProductQuery::SORT_ASC ), ]; $productQuery = new ProductQuery(null, null, $sortClauses); ``` # CreatedAt Sort Clause > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). CreatedAt Sort Clause The `CreatedAt` Sort Clause sorts search results by the date and time of the creation of a product. ## Arguments - (optional) `sortDirection` - `CreatedAt` constant, either `CreatedAt::SORT_ASC` or `CreatedAt::SORT_DESC` ## Example ```php use Ibexa\Contracts\ProductCatalog\Values\Product\ProductQuery; $productQuery = new ProductQuery( null, null, [ new \Ibexa\Contracts\ProductCatalog\Values\Product\Query\SortClause\CreatedAt( \Ibexa\Contracts\ProductCatalog\Values\Product\Query\SortClause\CreatedAt::SORT_ASC ), ] ); ``` # CustomPrice Sort Clause > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). CustomPrice Sort Clause The `CustomPrice` Sort Clause sorts search results by the product's custom price for a selected customer group. ## Arguments - `currency` - a `CurrencyInterface` object representing the currency to check price for - (optional) `sortDirection` - Query or LocationQuery constant, either `Query::SORT_ASC` or `Query::SORT_DESC` - (optional) `customerGroup` - a `CustomerGroupInterface` object representing the customer group to check prices for. If you don't provide a customer group, the query uses the group related to the current user. ## Limitations The `CustomPrice` Sort Clause isn't available in the Legacy Search engine. ## Example ```php use Ibexa\Contracts\ProductCatalog\Values\CurrencyInterface; use Ibexa\Contracts\ProductCatalog\Values\CustomerGroupInterface; use Ibexa\Contracts\ProductCatalog\Values\Product\ProductQuery; /** * @var CurrencyInterface $currency * @var CustomerGroupInterface $customerGroup */ $sortClauses = [ new \Ibexa\Contracts\ProductCatalog\Values\Product\Query\SortClause\CustomPrice( $currency, ProductQuery::SORT_ASC, $customerGroup ), ]; $productQuery = new ProductQuery(null, null, $sortClauses); ``` # ProductAvailability Sort Clause > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). ProductAvailability Sort Clause The `ProductAvailability` Sort Clause sorts search results by whether they have availability or not. ## Arguments - (optional) `sortDirection` - Query or LocationQuery constant, either `Query::SORT_ASC` or `Query::SORT_DESC` ## Example ```php use Ibexa\Contracts\ProductCatalog\Values\Product\ProductQuery; $query = new ProductQuery( null, null, [ new \Ibexa\Contracts\ProductCatalog\Values\Product\Query\SortClause\ProductAvailability(), ] ); ``` # ProductCode Sort Clause > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). ProductCode Sort Clause The `ProductCode` Sort Clause sorts search results by the product code. ## Arguments - (optional) `sortDirection` - Query or LocationQuery constant, either `Query::SORT_ASC` or `Query::SORT_DESC` ## Example ```php use Ibexa\Contracts\ProductCatalog\Values\Product\ProductQuery; $query = new ProductQuery( null, null, [ new \Ibexa\Contracts\ProductCatalog\Values\Product\Query\SortClause\ProductCode(), ] ); ``` # ProductName Sort Clause > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). ProductName Sort Clause The `ProductName` Sort Clause sorts search results by the Product code. ## Arguments - (optional) `sortDirection` - Query or LocationQuery constant, either `Query::SORT_ASC` or `Query::SORT_DESC` ## Example ```php use Ibexa\Contracts\ProductCatalog\Values\Product\ProductQuery; $query = new ProductQuery( null, null, [ new \Ibexa\Contracts\ProductCatalog\Values\Product\Query\SortClause\ProductName(), ] ); ``` # URL Sort Clauses > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). URL Sort Clauses URL Sort Clauses are the sorting options for URLs. They're only supported by [URL Search (`URLService::findUrls`)](https://doc.ibexa.co/en/saas/content_management/url_management/url_api/index.md). All URL Sort Clauses can take the following optional argument: - `sortDirection` - the direction of the sorting, either `\Ibexa\Contracts\Core\Repository\Values\URL\Query\SortClause::SORT_ASC` (default) or `\Ibexa\Contracts\Core\Repository\Values\URL\Query\SortClause::SORT_DESC` | Sort Clause | Sorting based on | | -------------------------------------------------------------------------------------------- | ---------------- | | [Id](https://doc.ibexa.co/en/saas/search/url_search_reference/id_url_sort_clause/index.md) | URL ID | | [URL](https://doc.ibexa.co/en/saas/search/url_search_reference/url_url_sort_clause/index.md) | URL address | # Id Sort Clause > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). Id Sort Clause The [`SortClause\Id` Sort Clause](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-Core-Repository-Values-URL-Query-SortClause-Id.html) sorts search results by the ID of the URL. ## Arguments - `sortDirection` (optional) - the direction of the sorting, either `\Ibexa\Contracts\Core\Repository\Values\URL\Query\SortClause::SORT_ASC` (default) or `\Ibexa\Contracts\Core\Repository\Values\URL\Query\SortClause::SORT_DESC` ## Example ```php use Ibexa\Contracts\Core\Repository\Values\URL\Query\SortClause; use Ibexa\Contracts\Core\Repository\Values\URL\URLQuery; // ... $query = new URLQuery(); $query->sortClauses = [new SortClause\Id()]; ``` # URL Sort Clause > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). URL Sort Clause The [`SortClause\Url` Sort Clause](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-Core-Repository-Values-URL-Query-SortClause-URL.html) sorts search results by the URLs. ## Arguments - `sortDirection` - the direction of the sorting, either `\Ibexa\Contracts\Core\Repository\Values\URL\Query\SortClause::SORT_ASC` (default) or `\Ibexa\Contracts\Core\Repository\Values\URL\Query\SortClause::SORT_DESC` ## Example ```php use Ibexa\Contracts\Core\Repository\Values\URL\Query\SortClause; use Ibexa\Contracts\Core\Repository\Values\URL\URLQuery; // ... $query = new URLQuery(); $query->sortClauses = [new SortClause\URL()]; ``` # Activity Log Search Sort Clauses reference > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). See [Searching in the Activity Log groups](https://doc.ibexa.co/en/saas/administration/recent_activity/recent_activity/#searching-in-the-activity-log-groups) for the whole API. Sort Clauses are found in the `Ibexa\Contracts\ActivityLog\Values\ActivityLog\SortClause` namespace. - `LoggedAtSortClause`: Sort Activity Log entries by their date and time, descending or ascending. # Action Configuration Search Sort Clauses reference > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). Sort Clauses available for Action Configuration search Sort Clauses are found in the `Ibexa\Contracts\ConnectorAi\ActionConfiguration\Query\SortClause` namespace, implementing the [SortClauseInterface](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-ConnectorAi-ActionConfiguration-Query-SortClauseInterface.html) interface: - [Enabled](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-ConnectorAi-ActionConfiguration-Query-SortClause-Enabled.html) - [Id](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-ConnectorAi-ActionConfiguration-Query-SortClause-Id.html) - [Identifier](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-ConnectorAi-ActionConfiguration-Query-SortClause-Identifier.html) The following example shows how to use them to sort the searched Action Configurations: ```php findActionConfigurations($query); ``` The search results are sorted by: - status, with enabled on top - identifier, in ascending order. # Aggregation reference > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). Aggregations help fine-tune search for content and Locations by grouping results into categories. [Aggregation](https://doc.ibexa.co/en/saas/search/search_api/#aggregation) is used to group search results into categories. There are three types of aggregations: - Term aggregations group by value and count object in each group - Range aggregations count values in specified ranges - Stats aggregations compute stats over numeric fields: minimum, average and maximum value, count, and sum of values > **Tip: Tip** > > Aggregations aren't available in the Legacy Search engine. ## Content aggregations | Name | Type | Based on | | -------------------------------------------------------------------------------------------------------------------------------------- | ----- | ------------------------------------- | | [ContentTypeTermAggregation](https://doc.ibexa.co/en/saas/search/aggregation_reference/contenttypeterm_aggregation/index.md) | Term | Content type | | [ContentTypeGroupTermAggregation](https://doc.ibexa.co/en/saas/search/aggregation_reference/contenttypegroupterm_aggregation/index.md) | Term | Content type group | | [DateMetadataRangeAggregation](https://doc.ibexa.co/en/saas/search/aggregation_reference/datemetadatarange_aggregation/index.md) | Range | Date metadata | | [LanguageTermAggregation](https://doc.ibexa.co/en/saas/search/aggregation_reference/languageterm_aggregation/index.md) | Term | Content language | | [LocationChildrenTermAggregation](https://doc.ibexa.co/en/saas/search/aggregation_reference/locationchildrenterm_aggregation/index.md) | Term | Children on a Location | | [ObjectStateTermAggregation](https://doc.ibexa.co/en/saas/search/aggregation_reference/objectstateterm_aggregation/index.md) | Term | Object state | | [RawRangeAggregation](https://doc.ibexa.co/en/saas/search/aggregation_reference/rawrange_aggregation/index.md) | Range | Search index field | | [RawStatsAggregation](https://doc.ibexa.co/en/saas/search/aggregation_reference/rawstats_aggregation/index.md) | Stats | Search index field | | [RawTermAggregation](https://doc.ibexa.co/en/saas/search/aggregation_reference/rawterm_aggregation/index.md) | Term | Search index field | | [SectionTermAggregation](https://doc.ibexa.co/en/saas/search/aggregation_reference/sectionterm_aggregation/index.md) | Term | Section | | [SubtreeTermAggregation](https://doc.ibexa.co/en/saas/search/aggregation_reference/subtreeterm_aggregation/index.md) | Term | Location subtree path | | [TaxonomyEntryIdAggregation](https://doc.ibexa.co/en/saas/search/aggregation_reference/taxonomyentryid_aggregation/index.md) | Term | Taxonomy entry | | [UserMetadataTermAggregation](https://doc.ibexa.co/en/saas/search/aggregation_reference/usermetadataterm_aggregation/index.md) | Term | Content owner/owner group or modifier | | [VisibilityTermAggregation](https://doc.ibexa.co/en/saas/search/aggregation_reference/visibilityterm_aggregation/index.md) | Term | Content/Location visibility | ## Field aggregations | Name | Type | Based on field | | ------------------------------------------------------------------------------------------------------------------------ | ----- | ---------------------------------------------------------------------------------------------------------------------- | | [AuthorTermAggregation](https://doc.ibexa.co/en/saas/search/aggregation_reference/authorterm_aggregation/index.md) | Term | [Author](https://doc.ibexa.co/en/saas/content_management/field_types/field_type_reference/authorfield/index.md) | | [CheckboxTermAggregation](https://doc.ibexa.co/en/saas/search/aggregation_reference/checkboxterm_aggregation/index.md) | Term | [Checkbox](https://doc.ibexa.co/en/saas/content_management/field_types/field_type_reference/checkboxfield/index.md) | | [CountryTermAggregation](https://doc.ibexa.co/en/saas/search/aggregation_reference/countryterm_aggregation/index.md) | Term | [Country](https://doc.ibexa.co/en/saas/content_management/field_types/field_type_reference/countryfield/index.md) | | [DateRangeAggregation](https://doc.ibexa.co/en/saas/search/aggregation_reference/daterange_aggregation/index.md) | Range | [Date](https://doc.ibexa.co/en/saas/content_management/field_types/field_type_reference/datefield/index.md) | | [DateTimeRangeAggregation](https://doc.ibexa.co/en/saas/search/aggregation_reference/datetimerange_aggregation/index.md) | Range | [DateTime](https://doc.ibexa.co/en/saas/content_management/field_types/field_type_reference/dateandtimefield/index.md) | | [FloatRangeAggregation](https://doc.ibexa.co/en/saas/search/aggregation_reference/floatrange_aggregation/index.md) | Range | [Float](https://doc.ibexa.co/en/saas/content_management/field_types/field_type_reference/floatfield/index.md) | | [FloatStatsAggregation](https://doc.ibexa.co/en/saas/search/aggregation_reference/floatstats_aggregation/index.md) | Stats | [Float](https://doc.ibexa.co/en/saas/content_management/field_types/field_type_reference/floatfield/index.md) | | [IntegerRangeAggregation](https://doc.ibexa.co/en/saas/search/aggregation_reference/integerrange_aggregation/index.md) | Range | [Integer](https://doc.ibexa.co/en/saas/content_management/field_types/field_type_reference/integerfield/index.md) | | [IntegerStatsAggregation](https://doc.ibexa.co/en/saas/search/aggregation_reference/integerstats_aggregation/index.md) | Stats | [Integer](https://doc.ibexa.co/en/saas/content_management/field_types/field_type_reference/integerfield/index.md) | | [KeywordTermAggregation](https://doc.ibexa.co/en/saas/search/aggregation_reference/keywordterm_aggregation/index.md) | Term | [Keyword](https://doc.ibexa.co/en/saas/content_management/field_types/field_type_reference/keywordfield/index.md) | | [SelectionTermAggregation](https://doc.ibexa.co/en/saas/search/aggregation_reference/selectionterm_aggregation/index.md) | Term | [Selection](https://doc.ibexa.co/en/saas/content_management/field_types/field_type_reference/selectionfield/index.md) | | [TimeRangeAggregation](https://doc.ibexa.co/en/saas/search/aggregation_reference/timerange_aggregation/index.md) | Range | [Time](https://doc.ibexa.co/en/saas/content_management/field_types/field_type_reference/timefield/index.md) | ## Product aggregations | Name | Type | Based on | | --------------------------------------------------------------------------------------------------------------------------------- | ------------ | ------------------------ | | [Product attribute](https://doc.ibexa.co/en/saas/search/aggregation_reference/product_attribute_aggregations/index.md) | Term / Range | Product attribute values | | [BasePriceStats](https://doc.ibexa.co/en/saas/search/aggregation_reference/basepricestats_aggregation/index.md) | Stats | Product base price | | [CustomPriceStats](https://doc.ibexa.co/en/saas/search/aggregation_reference/custompricestats_aggregation/index.md) | Stats | Product custom price | | [ProductAvailabilityTerm](https://doc.ibexa.co/en/saas/search/aggregation_reference/productavailabilityterm_aggregation/index.md) | Term | Product availability | | [ProductStockRange](https://doc.ibexa.co/en/saas/search/aggregation_reference/productstockrange_aggregation/index.md) | Range | Product stock | | [ProductPriceRange](https://doc.ibexa.co/en/saas/search/aggregation_reference/productpricerange_aggregation/index.md) | Range | Product price | | [ProductTypeTerm](https://doc.ibexa.co/en/saas/search/aggregation_reference/producttypeterm_aggregation/index.md) | Term | Product type | | [TaxonomyEntryIdAggregation](https://doc.ibexa.co/en/saas/search/aggregation_reference/taxonomyentryid_aggregation/index.md) | Term | Product category | # ContentTypeTermAggregation > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). ContentTypeTermAggregation The [ContentTypeTermAggregation](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-Core-Repository-Values-Content-Query-Aggregation-ContentTypeTermAggregation.html) aggregates search results by the content item's content type. ## Arguments - `name` - name of the Aggregation object ## Example ```php use Ibexa\Contracts\Core\Repository\Values\Content\Query; use Ibexa\Contracts\Core\Repository\Values\Content\Query\Aggregation; $query = new Query(); $query->aggregations[] = new Aggregation\ContentTypeTermAggregation('content_type'); ``` ## Settings You can define additional limits to the results using the `setLimit()` and `setMinCount()` methods. The following example limits the number of terms returned to 5 and only considers terms that have 10 or more results: ```php $aggregation = new //... $aggregation->setLimit(5); $aggregation->setMinCount(10); ``` # ContentTypeGroupTermAggregation > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). ContentTypeGroupTermAggregation The [ContentTypeGroupTermAggregation](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-Core-Repository-Values-Content-Query-Aggregation-ContentTypeGroupTermAggregation.html) aggregates search results by the content item's content type group. ## Arguments - `name` - name of the Aggregation object ## Example ```php use Ibexa\Contracts\Core\Repository\Values\Content\Query; use Ibexa\Contracts\Core\Repository\Values\Content\Query\Aggregation; $query = new Query(); $query->aggregations[] = new Aggregation\ContentTypeGroupTermAggregation('content_type_group'); ``` ## Settings You can define additional limits to the results using the `setLimit()` and `setMinCount()` methods. The following example limits the number of terms returned to 5 and only considers terms that have 10 or more results: ```php $aggregation = new //... $aggregation->setLimit(5); $aggregation->setMinCount(10); ``` # DateMetadataRangeAggregation > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). DateMetadataRangeAggregation The [DateMetadataRangeAggregation](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-Core-Repository-Values-Content-Query-Aggregation-Field-CountryTermAggregation.html) aggregates search results by the value of the content items' date metadata. ## Arguments - `name` - name of the Aggregation object - `type` - string representing the type of the Aggregation (`MODIFIED` or `PUBLISHED`) - `ranges` - array of Range objects that define the borders of the specific range sets ## Example ```php use Ibexa\Contracts\Core\Repository\Values\Content\Query; use Ibexa\Contracts\Core\Repository\Values\Content\Query\Aggregation; use Ibexa\Contracts\Core\Repository\Values\Content\Query\Aggregation\Range; $query = new Query(); $query->aggregations[] = new Aggregation\DateMetadataRangeAggregation( 'date_metadata', Aggregation\DateMetadataRangeAggregation::PUBLISHED, [ Range::ofDateTime(null, new DateTime('2020-06-01')), Range::ofDateTime(new DateTime('2020-06-01'), new DateTime('2020-12-31')), Range::ofDateTime(new DateTime('2020-12-31'), null), ] ); ``` # LanguageTermAggregation > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). LanguageTermAggregation The [LanguageTermAggregation](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-Core-Repository-Values-Content-Query-Aggregation-LanguageTermAggregation.html) aggregates search results by the content item's language. ## Arguments - `name` - name of the Aggregation object ## Example ```php use Ibexa\Contracts\Core\Repository\Values\Content\Query; use Ibexa\Contracts\Core\Repository\Values\Content\Query\Aggregation; $query = new Query(); $query->aggregations[] = new Aggregation\LanguageTermAggregation('language'); ``` ## Settings You can define additional limits to the results using the `setLimit()` and `setMinCount()` methods. The following example limits the number of terms returned to 5 and only considers terms that have 10 or more results: ```php $aggregation = new //... $aggregation->setLimit(5); $aggregation->setMinCount(10); ``` # LocationChildrenTermAggregation > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). LocationChildrenTermAggregation The [LocationChildrenTermAggregation](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-Core-Repository-Values-Content-Query-Aggregation-Location-LocationChildrenTermAggregation.html) aggregates search results by the number of children of a location. ## Arguments - `name` - name of the Aggregation object ## Example ```php use Ibexa\Contracts\Core\Repository\Values\Content\LocationQuery; use Ibexa\Contracts\Core\Repository\Values\Content\Query\Aggregation; $query = new LocationQuery(); $query->aggregations[] = new Aggregation\Location\LocationChildrenTermAggregation('location_children'); ``` ## Settings You can define additional limits to the results using the `setLimit()` and `setMinCount()` methods. The following example limits the number of terms returned to 5 and only considers terms that have 10 or more results: ```php $aggregation = new //... $aggregation->setLimit(5); $aggregation->setMinCount(10); ``` # ObjectStateTermAggregation > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). ObjectStateTermAggregation The [ObjectStateTermAggregation](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-Core-Repository-Values-Content-Query-Aggregation-ObjectStateTermAggregation.html) aggregates search results by the content item's object state. ## Arguments - `name` - name of the Aggregation object - `objectStateGroupIdentifier` - string representing the identifier of the object state group to aggregate results by ## Example ```php use Ibexa\Contracts\Core\Repository\Values\Content\Query; use Ibexa\Contracts\Core\Repository\Values\Content\Query\Aggregation; $query = new Query(); $query->aggregations[] = new Aggregation\ObjectStateTermAggregation('object_state', 'ibexa_lock'); ``` ## Settings You can define additional limits to the results using the `setLimit()` and `setMinCount()` methods. The following example limits the number of terms returned to 5 and only considers terms that have 10 or more results: ```php $aggregation = new //... $aggregation->setLimit(5); $aggregation->setMinCount(10); ``` # RawRangeAggregation > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). RawRangeAggregation The [RawRangeAggregation](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-Core-Repository-Values-Content-Query-Aggregation-RawRangeAggregation.html) aggregates search results by the value of the selected search index field. ## Arguments - `name` - name of the Aggregation object - `field` - string representing the search index field - `ranges` - array of Range objects that define the borders of the specific range sets ## Limitations > **Caution: Caution** > > To keep your project search engine independent, don't use the `RawRangeAggregation` Aggregation in production code. Valid use cases are: testing, or temporary (one-off) tools. ## Example ```php use Ibexa\Contracts\Core\Repository\Values\Content\LocationQuery; use Ibexa\Contracts\Core\Repository\Values\Content\Query\Aggregation; use Ibexa\Contracts\Core\Repository\Values\Content\Query\Aggregation\Range; $query = new LocationQuery(); $query->aggregations[] = new Aggregation\RawRangeAggregation('priority', 'priority_id', [ Range::ofInt(1, 10), Range::ofInt(10, 100), ]); ``` # RawStatsAggregation > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). RawStatsAggregation The [RawStatsAggregation](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-Core-Repository-Values-Content-Query-Aggregation-RawStatsAggregation.html) aggregates search results by the value of the selected search index field and provides statistical information for the values. You can use the provided getters to access the values: - sum (`getSum()`) - count of values (`getCount()`) - minimum value (`getMin()`) - maximum value (`getMax()`) - average (`getAvg()`) ## Arguments - `name` - name of the Aggregation object - `field` - string representing the search index field ## Limitations > **Caution: Caution** > > To keep your project search engine independent, don't use the `RawStatsAggregation` Aggregation in production code. Valid use cases are: testing, or temporary (one-off) tools. ## Example ```php use Ibexa\Contracts\Core\Repository\Values\Content\Query; use Ibexa\Contracts\Core\Repository\Values\Content\Query\Aggregation; $query = new Query(); $query->aggregations[] = new Aggregation\RawStatsAggregation('location_depth', 'depth_i'); ``` # RawTermAggregation > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). RawTermAggregation The [RawTermAggregation](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-Core-Repository-Values-Content-Query-Aggregation-RawTermAggregation.html) aggregates search results by the value of the selected search index field. ## Arguments - `name` - name of the Aggregation object - `field` - string representing the search index field ## Limitations > **Caution: Caution** > > To keep your project search engine independent, don't use the `RawTermAggregation` Aggregation in production code. Valid use cases are: testing, or temporary (one-off) tools. ## Example ```php use Ibexa\Contracts\Core\Repository\Values\Content\Query; use Ibexa\Contracts\Core\Repository\Values\Content\Query\Aggregation; $query = new Query(); $query->aggregations[] = new Aggregation\RawTermAggregation('content_per_content_type', 'content_type_id_id'); ``` ## Settings You can define additional limits to the results using the `setLimit()` and `setMinCount()` methods. The following example limits the number of terms returned to 5 and only considers terms that have 10 or more results: ```php $aggregation = new //... $aggregation->setLimit(5); $aggregation->setMinCount(10); ``` # SectionTermAggregation > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). SectionTermAggregation The [SectionTermAggregation](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-Core-Repository-Values-Content-Query-Aggregation-SectionTermAggregation.html) aggregates search results by the content item's section. ## Arguments - `name` - name of the Aggregation object ## Example ```php use Ibexa\Contracts\Core\Repository\Values\Content\Query; use Ibexa\Contracts\Core\Repository\Values\Content\Query\Aggregation; $query = new Query(); $query->aggregations[] = new Aggregation\SectionTermAggregation('section'); ``` ## Settings You can define additional limits to the results using the `setLimit()` and `setMinCount()` methods. The following example limits the number of terms returned to 5 and only considers terms that have 10 or more results: ```php $aggregation = new //... $aggregation->setLimit(5); $aggregation->setMinCount(10); ``` # SubtreeTermAggregation > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). SubtreeTermAggregation The [SubtreeTermAggregation](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-Core-Repository-Values-Content-Query-Aggregation-Location-SubtreeTermAggregation.html) aggregates search results by the location's subtree path. ## Arguments - `name` - name of the Aggregation object - `pathString` - string representing the pathstring to aggregate results by ## Example ```php use Ibexa\Contracts\Core\Repository\Values\Content\Query; use Ibexa\Contracts\Core\Repository\Values\Content\Query\Aggregation; $query = new Query(); $query->aggregations[] = new Aggregation\Location\SubtreeTermAggregation('pathstring', '/1/2/'); ``` ## Settings You can define additional limits to the results using the `setLimit()` and `setMinCount()` methods. The following example limits the number of terms returned to 5 and only considers terms that have 10 or more results: ```php $aggregation = new //... $aggregation->setLimit(5); $aggregation->setMinCount(10); ``` # TaxonomyEntryIdAggregation > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). TaxonomyEntryIdAggregation The `TaxonomyEntryIdAggregation` aggregates search results by the content item's taxonomy entry or a product's category. ## Arguments - `name` - name of the Aggregation object - `taxonomyIdentifier` - identifier of the taxonomy to aggregate results by ## Example ```php use Ibexa\Contracts\Core\Repository\Values\Content\Query; use Ibexa\Contracts\Taxonomy\Search\Query\Aggregation as Aggregation; $query = new Query(); $query->aggregations[] = new Aggregation\TaxonomyEntryIdAggregation('taxonomy', 'tags'); ``` ```php use Ibexa\Contracts\ProductCatalog\Values\Product\ProductQuery; use Ibexa\Contracts\Taxonomy\Search\Query\Aggregation\TaxonomyEntryIdAggregation; $query = new ProductQuery(); $query->setAggregations([new TaxonomyEntryIdAggregation('categories', 'product_categories')]); ``` # UserMetadataTermAggregation > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). UserMetadataTermAggregation The [UserMetadataTermAggregation](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-Core-Repository-Values-Content-Query-Aggregation-UserMetadataTermAggregation.html) aggregates search results by the User content item's metadata. ## Arguments - `name` - name of the Aggregation object ## Example ```php use Ibexa\Contracts\Core\Repository\Values\Content\Query; use Ibexa\Contracts\Core\Repository\Values\Content\Query\Aggregation; $query = new Query(); $query->aggregations[] = new Aggregation\UserMetadataTermAggregation('user_metadata'); ``` ## Settings You can define additional limits to the results using the `setLimit()` and `setMinCount()` methods. The following example limits the number of terms returned to 5 and only considers terms that have 10 or more results: ```php $aggregation = new //... $aggregation->setLimit(5); $aggregation->setMinCount(10); ``` # VisibilityTermAggregation > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). VisibilityTermAggregation The [VisibilityTermAggregation](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-Core-Repository-Values-Content-Query-Aggregation-VisibilityTermAggregation.html) aggregates search results by the content item's visibility. ## Arguments - `name` - name of the Aggregation object ## Example ```php use Ibexa\Contracts\Core\Repository\Values\Content\Query; use Ibexa\Contracts\Core\Repository\Values\Content\Query\Aggregation; $query = new Query(); $query->aggregations[] = new Aggregation\VisibilityTermAggregation('visibility'); ``` ## Settings You can define additional limits to the results using the `setLimit()` and `setMinCount()` methods. The following example limits the number of terms returned to 5 and only considers terms that have 10 or more results: ```php $aggregation = new //... $aggregation->setLimit(5); $aggregation->setMinCount(10); ``` # AuthorTermAggregation > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). AuthorTermAggregation The field-based [AuthorTermAggregation](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-Core-Repository-Values-Content-Query-Aggregation-Field-AuthorTermAggregation.html) aggregates search results by the value of the Author field. ## Arguments - `name` - name of the Aggregation - `contentTypeIdentifier` - string representing the content type identifier - `fieldDefinitionIdentifier` - string representing the Field Definition identifier ## Example ```php use Ibexa\Contracts\Core\Repository\Values\Content\Query; use Ibexa\Contracts\Core\Repository\Values\Content\Query\Aggregation; $query = new Query(); $query->aggregations[] = new Aggregation\Field\AuthorTermAggregation('author', 'article', 'authors'); ``` ## Settings You can define additional limits to the results using the `setLimit()` and `setMinCount()` methods. The following example limits the number of terms returned to 5 and only considers terms that have 10 or more results: ```php $aggregation = new //... $aggregation->setLimit(5); $aggregation->setMinCount(10); ``` # CheckboxTermAggregation > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). CheckboxTermAggregation The field-based [CheckboxTermAggregation](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-Core-Repository-Values-Content-Query-Aggregation-Field-CheckboxTermAggregation.html) aggregates search results by the value of the Checkbox field. ## Arguments - `name` - name of the Aggregation - `contentTypeIdentifier` - string representing the content type identifier - `fieldDefinitionIdentifier` - string representing the Field Definition identifier ## Example ```php use Ibexa\Contracts\Core\Repository\Values\Content\Query; use Ibexa\Contracts\Core\Repository\Values\Content\Query\Aggregation; $query = new Query(); $query->aggregations[] = new Aggregation\Field\CheckboxTermAggregation('checkbox', 'article', 'enable_comments'); ``` ## Settings You can define additional limits to the results using the `setLimit()` and `setMinCount()` methods. The following example limits the number of terms returned to 5 and only considers terms that have 10 or more results: ```php $aggregation = new //... $aggregation->setLimit(5); $aggregation->setMinCount(10); ``` # CountryTermAggregation > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). CountryTermAggregation The field-based [CountryTermAggregation](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-Core-Repository-Values-Content-Query-Aggregation-Field-CountryTermAggregation.html) aggregates search results by the value of the Country field. ## Arguments - `name` - name of the Aggregation - `contentTypeIdentifier` - string representing the content type identifier - `fieldDefinitionIdentifier` - string representing the Field Definition identifier ## Example ```php use Ibexa\Contracts\Core\Repository\Values\Content\Query; use Ibexa\Contracts\Core\Repository\Values\Content\Query\Aggregation; $query = new Query(); $query->aggregations[] = new Aggregation\Field\CountryTermAggregation('country', 'article', 'country'); ``` ## Settings You can define additional limits to the results using the `setLimit()` and `setMinCount()` methods. The following example limits the number of terms returned to 5 and only considers terms that have 10 or more results: ```php $aggregation = new //... $aggregation->setLimit(5); $aggregation->setMinCount(10); ``` # DateRangeAggregation > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). DateRangeAggregation The field-based [DateRangeAggregation](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-Core-Repository-Values-Content-Query-Aggregation-Field-DateRangeAggregation.html) aggregates search results by the value of the Date, DateTime, or Time field. ## Arguments - `name` - name of the Aggregation - `contentTypeIdentifier` - string representing the content type identifier - `fieldDefinitionIdentifier` - string representing the Field Definition identifier - `ranges` - array of Range objects that define the borders of the specific range sets ## Example ```php use Ibexa\Contracts\Core\Repository\Values\Content\Query; use Ibexa\Contracts\Core\Repository\Values\Content\Query\Aggregation; use Ibexa\Contracts\Core\Repository\Values\Content\Query\Aggregation\Range; $query = new Query(); $query->aggregations[] = new Aggregation\Field\DateRangeAggregation( 'date', 'event', 'event_date', [ Range::ofDateTime(null, new DateTime('2020-06-01')), Range::ofDateTime(new DateTime('2020-06-01'), new DateTime('2020-12-31')), Range::ofDateTime(new DateTime('2020-12-31'), null), ] ); ``` # DateTimeRangeAggregation > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). DateTimeRangeAggregation The field-based [DateTimeRangeAggregation](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-Core-Repository-Values-Content-Query-Aggregation-Field-DateTimeRangeAggregation.html) aggregates search results by the value of the Date, DateTime, or Time field. ## Arguments - `name` - name of the Aggregation - `contentTypeIdentifier` - string representing the content type identifier - `fieldDefinitionIdentifier` - string representing the Field Definition identifier - `ranges` - array of Range objects that define the borders of the specific range sets ## Example ```php use Ibexa\Contracts\Core\Repository\Values\Content\Query; use Ibexa\Contracts\Core\Repository\Values\Content\Query\Aggregation; use Ibexa\Contracts\Core\Repository\Values\Content\Query\Aggregation\Range; $query = new Query(); $query->aggregations[] = new Aggregation\Field\DateTimeRangeAggregation( 'date', 'event', 'event_date', [ Range::ofDateTime(null, new DateTime('2020-06-01')), Range::ofDateTime(new DateTime('2020-06-01'), new DateTime('2020-12-31')), Range::ofDateTime(new DateTime('2020-12-31'), null), ] ); ``` # FloatRangeAggregation > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). FloatRangeAggregation The field-based [FloatRangeAggregation](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-Core-Repository-Values-Content-Query-Aggregation-Field-FloatRangeAggregation.html) aggregates search results by the value of the Float field. ## Arguments - `name` - name of the Aggregation - `contentTypeIdentifier` - string representing the content type identifier - `fieldDefinitionIdentifier` - string representing the Field Definition identifier - `ranges` - array of Range objects that define the borders of the specific range sets ## Example ```php use Ibexa\Contracts\Core\Repository\Values\Content\Query; use Ibexa\Contracts\Core\Repository\Values\Content\Query\Aggregation; use Ibexa\Contracts\Core\Repository\Values\Content\Query\Aggregation\Range; $query = new Query(); $query->aggregations[] = new Aggregation\Field\FloatRangeAggregation( 'float', 'product', 'weight', [ Range::ofFloat(null, 0.25), Range::ofFloat(0.25, 0.75), Range::ofFloat(0.75, null), ] ); ``` # FloatStatsAggregation > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). FloatStatsAggregation The field-based [FloatStatsAggregation](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-Core-Repository-Values-Content-Query-Aggregation-Field-FloatStatsAggregation.html) aggregates search results by the value of the Float field and provides statistical information for the values. You can use the provided getters to access the values: - sum (`getSum()`) - count of values (`getCount()`) - minimum value (`getMin()`) - maximum value (`getMax()`) - average (`getAvg()`) ## Arguments - `name` - name of the Aggregation - `contentTypeIdentifier` - string representing the content type identifier - `fieldDefinitionIdentifier` - string representing the Field Definition identifier ## Example ```php use Ibexa\Contracts\Core\Repository\Values\Content\Query; use Ibexa\Contracts\Core\Repository\Values\Content\Query\Aggregation; $query = new Query(); $query->aggregations[] = new Aggregation\Field\FloatStatsAggregation('float', 'product', 'weight'); ``` # IntegerRangeAggregation > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). IntegerRangeAggregation The field-based [IntegerRangeAggregation](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-Core-Repository-Values-Content-Query-Aggregation-Field-IntegerRangeAggregation.html) aggregates search results by the value of the Integer field. ## Arguments - `name` - name of the Aggregation - `contentTypeIdentifier` - string representing the content type identifier - `fieldDefinitionIdentifier` - string representing the Field Definition identifier - `ranges` - array of Range objects that define the borders of the specific range sets ## Example ```php use Ibexa\Contracts\Core\Repository\Values\Content\Query; use Ibexa\Contracts\Core\Repository\Values\Content\Query\Aggregation; use Ibexa\Contracts\Core\Repository\Values\Content\Query\Aggregation\Range; $query = new Query(); $query->aggregations[] = new Aggregation\Field\IntegerRangeAggregation( 'integer', 'product', 'amount', [ Range::ofInt(null, 12), Range::ofInt(12, 24), Range::ofInt(24, null), ] ); ``` # IntegerStatsAggregation > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). IntegerStatsAggregation The field-based [IntegerStatsAggregation](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-Core-Repository-Values-Content-Query-Aggregation-Field-IntegerStatsAggregation.html) aggregates search results by the value of the Integer field and provides statistical information for the values. You can use the provided getters to access the values: - sum (`getSum()`) - count of values (`getCount()`) - minimum value (`getMin()`) - maximum value (`getMax()`) - average (`getAvg()`) ## Arguments - `name` - name of the Aggregation - `contentTypeIdentifier` - string representing the content type identifier - `fieldDefinitionIdentifier` - string representing the Field Definition identifier ## Example ```php use Ibexa\Contracts\Core\Repository\Values\Content\Query; use Ibexa\Contracts\Core\Repository\Values\Content\Query\Aggregation; $query = new Query(); $query->aggregations[] = new Aggregation\Field\IntegerStatsAggregation('integer', 'product', 'amount'); ``` # KeywordTermAggregation > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). KeywordTermAggregation The field-based [KeywordTermAggregation](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-Core-Repository-Values-Content-Query-Aggregation-Field-KeywordTermAggregation.html) aggregates search results by the value of the Keyword field. ## Arguments - `name` - name of the Aggregation - `contentTypeIdentifier` - string representing the content type identifier - `fieldDefinitionIdentifier` - string representing the Field Definition identifier ## Example ```php use Ibexa\Contracts\Core\Repository\Values\Content\Query; use Ibexa\Contracts\Core\Repository\Values\Content\Query\Aggregation; $query = new Query(); $query->aggregations[] = new Aggregation\Field\KeywordTermAggregation('keyword', 'article', 'tags'); ``` ## Settings You can define additional limits to the results using the `setLimit()` and `setMinCount()` methods. The following example limits the number of terms returned to 5 and only considers terms that have 10 or more results: ```php $aggregation = new //... $aggregation->setLimit(5); $aggregation->setMinCount(10); ``` # SelectionTermAggregation > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). SelectionTermAggregation The field-based [SelectionTermAggregation](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-Core-Repository-Values-Content-Query-Aggregation-Field-SelectionTermAggregation.html) aggregates search results by the value of the Selection field. ## Arguments - `name` - name of the Aggregation - `contentTypeIdentifier` - string representing the content type identifier - `fieldDefinitionIdentifier` - string representing the Field Definition identifier ## Example ```php use Ibexa\Contracts\Core\Repository\Values\Content\Query; use Ibexa\Contracts\Core\Repository\Values\Content\Query\Aggregation; $query = new Query(); $query->aggregations[] = new Aggregation\Field\SelectionTermAggregation('selection', 'article', 'select'); ``` ## Settings You can define additional limits to the results using the `setLimit()` and `setMinCount()` methods. The following example limits the number of terms returned to 5 and only considers terms that have 10 or more results: ```php $aggregation = new //... $aggregation->setLimit(5); $aggregation->setMinCount(10); ``` # TimeRangeAggregation > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). TimeRangeAggregation The field-based [TimeRangeAggregation](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-Core-Repository-Values-Content-Query-Aggregation-Field-TimeRangeAggregation.html) aggregates search results by the value of the Date, DateTime, or Time field. ## Arguments - `name` - name of the Aggregation - `contentTypeIdentifier` - string representing the content type identifier - `fieldDefinitionIdentifier` - string representing the Field Definition identifier - `ranges` - array of Range objects that define the borders of the specific range sets ## Example ```php use Ibexa\Contracts\Core\Repository\Values\Content\Query; use Ibexa\Contracts\Core\Repository\Values\Content\Query\Aggregation; use Ibexa\Contracts\Core\Repository\Values\Content\Query\Aggregation\Range; $timestamp = mktime(14, 0, 0); if ($timestamp === false) { throw new RuntimeException('Failed to create timestamp with mktime.'); } $query = new Query(); $query->aggregations[] = new Aggregation\Field\TimeRangeAggregation( 'date', 'event', 'event_time', [ Range::ofInt(null, $timestamp), Range::ofInt($timestamp, null), ] ); ``` # Product attribute aggregations > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). Product attribute aggregations aggregate search results by the value of the product's attributes. Product attribute aggregations aggregate search results by the value of the product's attributes. Depending on attribute type, the following aggregations are available: - `ProductAttributeBooleanAggregation` - `ProductAttributeColorAggregation` - `ProductAttributeFloatAggregation` - `ProductAttributeFloatRangeAggregation` - `ProductAttributeIntegerAggregation` - `ProductAttributeIntegerRangeAggregation` - `ProductAttributeSelectionAggregation` ## Arguments - `name` - name of the Aggregation - `attributeDefinitionIdentifier` - identifier of the attribute Range aggregations (`ProductAttributeFloatRangeAggregation` and `ProductAttributeIntegerRangeAggregation`) additionally take: - `ranges` - array of Range objects that define the borders of the specific range sets ## Example ```php use Ibexa\Contracts\ProductCatalog\Values\Product\ProductQuery; use Ibexa\Contracts\ProductCatalog\Values\Product\Query\Aggregation\AttributeSelectionTermAggregation; $query = new ProductQuery(); $query->setAggregations([ new AttributeSelectionTermAggregation('skin', 'skin_type'), ]); ``` ```php use Ibexa\Contracts\Core\Repository\Values\Content\Query\Aggregation\Range; use Ibexa\Contracts\ProductCatalog\Values\Product\ProductQuery; use Ibexa\Contracts\ProductCatalog\Values\Product\Query\Aggregation\AttributeIntegerRangeAggregation; $query = new ProductQuery(); $query->setAggregations([ new AttributeIntegerRangeAggregation('buttons', 'number_of_buttons', [ Range::ofInt(null, 5), Range::ofInt(5, 10), Range::ofInt(10, null), ]), ]); ``` # BasePriceStatsAggregation > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). BasePriceStatsAggregation The BasePriceStatsAggregation aggregates search results by the value of the product's price can provides statistical information for the values. You can use the provided getters to access the values: - sum (`getSum()`) - count of values (`getCount()`) - minimum value (`getMin()`) - maximum value (`getMax()`) - average (`getAvg()`) ## Arguments - `name` - name of the Aggregation - `\Ibexa\Contracts\ProductCatalog\Values\CurrencyInterface` - currency of the price ## Example ```php use Ibexa\Contracts\ProductCatalog\Values\CurrencyInterface; use Ibexa\Contracts\ProductCatalog\Values\Product\ProductQuery; use Ibexa\Contracts\ProductCatalog\Values\Product\Query\Aggregation\BasePriceStatsAggregation; /** @var CurrencyInterface $currency */ $query = new ProductQuery(); $query->setAggregations([ new BasePriceStatsAggregation('base_price_stats_aggregation', $currency), ]); ``` # CustomPriceStatsAggregation > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). CustomPriceStatsAggregation The CustomPriceStatsAggregation aggregates search results by the value of the custom product's price and provides statistical information for the values. You can use the provided getters to access the values: - sum (`getSum()`) - count of values (`getCount()`) - minimum value (`getMin()`) - maximum value (`getMax()`) - average (`getAvg()`) ## Arguments - `name` - name of the Aggregation - `\Ibexa\Contracts\ProductCatalog\Values\CurrencyInterface` - currency of the price - `\Ibexa\Contracts\ProductCatalog\Values\CustomerGroupInterface|null` - customer group that defines custom pricing, by default it's the one assigned to current user ## Example ```php use Ibexa\Contracts\ProductCatalog\Values\CurrencyInterface; use Ibexa\Contracts\ProductCatalog\Values\CustomerGroupInterface; use Ibexa\Contracts\ProductCatalog\Values\Product\ProductQuery; use Ibexa\Contracts\ProductCatalog\Values\Product\Query\Aggregation\CustomPriceStatsAggregation; /** * @var CurrencyInterface $currency * @var CustomerGroupInterface $customerGroup */ $query = new ProductQuery(); $query->setAggregations([ new CustomPriceStatsAggregation('custom_price_stats_aggregation', $currency, $customerGroup), ]); ``` # ProductAvailabilityTerm > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). ProductAvailabilityTerm The ProductAvailabilityTermAggregation aggregates search results by product availability (available/unavailable). ## Arguments - `name` - name of the Aggregation object ## Example ```php use Ibexa\Contracts\ProductCatalog\Values\Product\ProductQuery; use Ibexa\Contracts\ProductCatalog\Values\Product\Query\Aggregation\ProductAvailabilityTermAggregation; $query = new ProductQuery(); $query->setAggregations([ new ProductAvailabilityTermAggregation('product_availability'), ]); ``` # ProductStockRangeAggregation > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). ProductStockRangeAggregation The ProductStockRangeAggregation aggregates search results by products' numerical stock. ## Arguments - `name` - name of the Aggregation - `ranges` - array of Range objects that define the borders of the specific range sets ## Example ```php use Ibexa\Contracts\Core\Repository\Values\Content\Query\Aggregation\Range; use Ibexa\Contracts\ProductCatalog\Values\Product\ProductQuery; use Ibexa\Contracts\ProductCatalog\Values\Product\Query\Aggregation\ProductStockRangeAggregation; $productQuery = new ProductQuery(); $productQuery->setAggregations([ new ProductStockRangeAggregation('stock', [ Range::ofInt(null, 10), Range::ofInt(10, 100), Range::ofInt(100, null), ]), ]); ``` # ProductPriceRangeAggregation > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). ProductPriceRangeAggregation The ProductPriceRangeAggregation aggregates search results by the value of the product's price. ## Arguments - `name` - name of the Aggregation - `currencyCode` - currency code of the price - `ranges` - array of Range objects that define the borders of the specific range sets ## Example ```php use Ibexa\Contracts\Core\Repository\Values\Content\Query\Aggregation\Range; use Ibexa\Contracts\ProductCatalog\Values\Product\ProductQuery; use Ibexa\Contracts\ProductCatalog\Values\Product\Query\Aggregation\ProductPriceRangeAggregation; $query = new ProductQuery(); $query->setAggregations([ new ProductPriceRangeAggregation('price', 'PLN', [ Range::ofInt(0, 10000), Range::ofInt(10000, null), ]), ]); ``` # ProductTypeTerm > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). ProductTypeTerm The ProductTypeTermAggregation aggregates search results by the product type. ## Arguments - `name` - name of the Aggregation object ## Example ```php use Ibexa\Contracts\ProductCatalog\Values\Product\ProductQuery; use Ibexa\Contracts\ProductCatalog\Values\Product\Query\Aggregation\ProductTypeTermAggregation; $query = new ProductQuery(); $query->setAggregations([ new ProductTypeTermAggregation('product_type'), ]); ``` # Embeddings search reference > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). Embedding queries, embedding configuration, providers, and embedding search fields Embeddings provide vector representations of content or text, enabling [semantic similarity search](https://doc.ibexa.co/en/saas/search/search_api/#search-with-embeddings). Foundational abstractions are provided for embedding-based search, while embedding providers generate vector representations. Searching with embeddings is designed for use with the [Taxonomy suggestions](https://doc.ibexa.co/en/saas/content_management/taxonomy/taxonomy/#taxonomy-suggestions) feature. The [`Ibexa\Contracts\Taxonomy\Search\Query\Value\TaxonomyEmbedding`](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-Taxonomy-Search-Query-Value-TaxonomyEmbedding.html) class allows embedding queries to target taxonomy data. > **Note: Feature support** > > Searching with embeddings requires a search engine that supports it, such as Elasticsearch or Solr 9.8.1+. ## Core query objects ### EmbeddingQuery - [`Ibexa\Contracts\Core\Repository\Values\Content\EmbeddingQuery`](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-Core-Repository-Values-Content-EmbeddingQuery.html) represents a semantic similarity search request. It encapsulates an [Embedding](#embedding) instance and supports pagination, aggregations, and result counting through the same API as standard content queries. > **Note: Embedding query properties** > > Embedding queries do not use criteria for similarity, but for additional filtering applied through the query filter. Also, embedding queries do not allow standard Query properties supported by [search engines](https://doc.ibexa.co/en/saas/search/search_engines/search_engines/index.md) other than the Legacy Search, such as `query`, `sortClauses`, or `spellcheck`. - [EmbeddingQueryBuilder](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-Core-Repository-Values-Content-EmbeddingQueryBuilder.html) is a builder for constructing `EmbeddingQuery` instances. It helps construct queries consistently and integrates embedding queries with the search query pipeline. You must provide the required embedding value by using the `withEmbedding` method ### Embedding - [`Ibexa\Contracts\Core\Repository\Values\Content\Query\Embedding`](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-Core-Repository-Values-Content-Query-Embedding.html) represents the vector input used for similarity search. It stores embedding values as float arrays, while providers generate those vectors from text input ## Query execution Embedding queries are executed by the search engine by using the configured embedding model and provider. At runtime, the system resolves the appropriate embedding provider and ensures that the embedding vector is compatible with the configured model. Runtime validation includes validating vector dimensionality and selecting the correct indexed field for similarity search. Field selection is determined by the configured embedding model and backend specific query mapping, while vector dimensionality is validated when the query reaches the search engine. ## Embedding providers Embedding providers implement the contract for generating vector representations of input data. Out of the box, embedding search integration is provided for `TaxonomyEmbedding`. If you use a custom embedding value type, implement matching embedding visitors for your [search engine](https://doc.ibexa.co/en/saas/search/search_engines/search_engines/index.md). Otherwise, query execution may fail due to no visitor available. - [`Ibexa\Contracts\Core\Search\Embedding\EmbeddingProviderInterface`](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-Core-Search-Embedding-EmbeddingProviderInterface.html) generates embeddings for the provided text or other input - [`Ibexa\Contracts\Core\Search\Embedding\EmbeddingProviderRegistryInterface`](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-Core-Search-Embedding-EmbeddingProviderRegistryInterface.html) lists available embedding providers or gets one by its identifier - [`Ibexa\Contracts\Core\Search\Embedding\EmbeddingProviderResolverInterface`](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-Core-Search-Embedding-EmbeddingProviderResolverInterface.html) determines the embedding provider to be used for generating embeddings based on the system configuration, or a demand passed through the `resolveByModelIdentifier` method ## Configuration Models used to resolve embedding queries must be configured per SiteAccess in [system configuration](https://doc.ibexa.co/en/saas/administration/configuration/configuration/index.md). Each entry defines the model's name, vector dimensionality, the field suffix, and the embedding provider that generates vectors. Field suffixes assigned to the models must be unique, as they become part of the indexed field name. You select the default model by setting a value in the `default_embedding_model` key. ```yaml ibexa: system: default: embedding_models: text-embedding-3-small: name: 'text-embedding-3-small' dimensions: 1536 field_suffix: '3small' embedding_provider: 'ibexa_openai' default_embedding_model: text-embedding-ada-002 ``` For a real-life example of embedding models configuration, see [Taxonomy suggestions](https://doc.ibexa.co/en/saas/content_management/taxonomy/taxonomy/#change-embedding-generation-models-or-embedding-provider). - [EmbeddingConfigurationInterface](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-Core-Search-Embedding-EmbeddingConfigurationInterface.html) allows access to the embedding model configuration in the system (for example, list of available models, default model name, default provider, field suffix, and so on) ## Embedding fields Embedding vectors are stored in dedicated search fields. These fields can be used by the search engine to perform vector similarity comparisons when embedding queries are executed. ```php create(); echo $embeddingField->getType(); // for example, "ibexa_dense_vector_model_123" // Create a custom embedding field with a specific type $customField = $factory->create('custom_embedding_type'); echo $customField->getType(); // "custom_embedding_type" ``` Once you create a field, subscribe to the `ContentIndexCreateEvent` indexing event that [adds the field to the index](https://doc.ibexa.co/en/saas/search/extensibility/index_custom_elasticsearch_data/index.md). - [`Ibexa\Contracts\Core\Search\FieldType\EmbeddingFieldFactory`](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-Core-Search-FieldType-EmbeddingFieldFactory.html) creates dedicated search fields that store embedding vectors ## Validation - [`Ibexa\Contracts\Core\Repository\Values\Content\QueryValidatorInterface`](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-Core-Repository-Values-Content-QueryValidatorInterface.html) validates embedding query structure before execution # Search in trash reference > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). Trash Search Criteria and Sort Clauses help define and fine-tune search queries for content in trash. When you [search for content items that are held in trash](https://doc.ibexa.co/en/saas/search/search_api/#search-in-trash), you can apply only a limited subset of Search Criteria and Sort Clauses which can be used by [`Ibexa\Contracts\Core\Repository\TrashService::findTrashItems`](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-Core-Repository-TrashService.html#method_findTrashItems). Some sort clauses are exclusive to trash search. ## Search Criteria - [ContentName](https://doc.ibexa.co/en/saas/search/criteria_reference/contentname_criterion/index.md) - [ContentTypeId](https://doc.ibexa.co/en/saas/search/criteria_reference/contenttypeid_criterion/index.md) - [DateMetadata](https://doc.ibexa.co/en/saas/search/criteria_reference/datemetadata_criterion/index.md) (which can use the additional exclusive target `DateMetadata::TRASHED`) - [MatchAll](https://doc.ibexa.co/en/saas/search/criteria_reference/matchall_criterion/index.md) - [MatchNone](https://doc.ibexa.co/en/saas/search/criteria_reference/matchnone_criterion/index.md) - [SectionId](https://doc.ibexa.co/en/saas/search/criteria_reference/sectionid_criterion/index.md) - [UserMetadata](https://doc.ibexa.co/en/saas/search/criteria_reference/usermetadata_criterion/index.md) ## Logical operators - [LogicalAnd](https://doc.ibexa.co/en/saas/search/criteria_reference/logicaland_criterion/index.md) - [LogicalNot](https://doc.ibexa.co/en/saas/search/criteria_reference/logicalor_criterion/index.md) - [LogicalOr](https://doc.ibexa.co/en/saas/search/criteria_reference/logicalor_criterion/index.md) ## Sort Clauses - [ContentName](https://doc.ibexa.co/en/saas/search/sort_clause_reference/contentname_sort_clause/index.md) - [ContentTypeName](https://doc.ibexa.co/en/saas/search/sort_clause_reference/contenttypename_sort_clause/index.md) - [DateTrashed](https://doc.ibexa.co/en/saas/search/sort_clause_reference/datetrashed_sort_clause/index.md) - [Depth](https://doc.ibexa.co/en/saas/search/sort_clause_reference/depth_sort_clause/index.md) - [Path](https://doc.ibexa.co/en/saas/search/sort_clause_reference/path_sort_clause/index.md) - [Priority](https://doc.ibexa.co/en/saas/search/sort_clause_reference/priority_sort_clause/index.md) - [SectionName](https://doc.ibexa.co/en/saas/search/sort_clause_reference/sectionname_sort_clause/index.md) - [UserLogin](https://doc.ibexa.co/en/saas/search/sort_clause_reference/userlogin_sort_clause/index.md) # Create custom Search Criterion > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). Create custom Search Criterion to use with Solr and Elasticsearch search engines. To provide support for a custom Search Criterion, do the following. ## Create Criterion class First, create a `CameraManufacturerCriterion.php` file that contains the Criterion class: ```php 'exif_camera_manufacturer_id:"' . $this->escapeQuote((string) $value) . '"', $criterion->value ); return '(' . implode(' OR ', $expressions) . ')'; } } ``` **Elasticsearch** ```php [ 'exif_camera_manufacturer_id' => (array)$criterion->value, ], ]; } } ``` Finally, register the visitor as a service. Search Criteria can be valid for both Content and Location search. To choose the search type, use either `content` or `location` in the tag when registering the visitor as a service: **Solr** ```yaml services: App\Query\Criterion\Solr\CameraManufacturerVisitor: tags: - { name: ibexa.search.solr.query.content.criterion.visitor } - { name: ibexa.search.solr.query.location.criterion.visitor } ``` **Elasticsearch** ```yaml services: App\Query\Criterion\Elasticsearch\CameraManufacturerVisitor: tags: - { name: ibexa.search.elasticsearch.query.content.criterion.visitor } - { name: ibexa.search.elasticsearch.query.location.criterion.visitor } ``` This example pretends a new `exif_camera_manufacturer_id` data is indexed. For more information about indexing new additional data, see [Solr document field mappers](https://doc.ibexa.co/en/saas/search/extensibility/solr_document_field_mappers/index.md) or [Index custom Elasticsearch data](https://doc.ibexa.co/en/saas/search/extensibility/index_custom_elasticsearch_data/index.md). # Create custom Sort Clause > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). Create custom Sort Clause to use with Solr and Elasticsearch search engines. To create a custom Sort Clause, do the following. ## Create Sort Clause class First, add a `ScoreSortClause.php` file with the Sort Clause class: ```php getDirection($sortClause); } } ``` **Elasticsearch** ```php direction === Query::SORT_ASC ? 'asc' : 'desc'; return [ '_score' => [ 'order' => $order, ], ]; } } ``` The `supports()` method checks if the implementation can handle the given Sort Clause. The `visit()` method contains the logic that translates Sort Clause information into data understandable by the search engine. The `visit()` method takes the Sort Clause visitor, the Sort Clause itself and the language filter as arguments. Finally, register the visitor as a service. Sort Clauses can be valid for both content and Location search. To choose the search type, use either `content` or `location` in the tag when registering the visitor as a service: **Solr** ```yaml services: App\Query\SortClause\Solr\ScoreVisitor: tags: - { name: ibexa.search.solr.query.content.sort_clause.visitor } - { name: ibexa.search.solr.query.location.sort_clause.visitor } ``` **Elasticsearch** ```yaml services: App\Query\SortClause\Elasticsearch\ScoreVisitor: tags: - { name: ibexa.search.elasticsearch.query.content.sort_clause.visitor } - { name: ibexa.search.elasticsearch.query.location.sort_clause.visitor } ``` # Create custom Aggregation > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). Create custom Aggregation to use with Solr and Elasticsearch search engines. ## Create aggregation class To create a custom Aggregation, create an aggregation class. In the following example, an aggregation groups the location query results by the location priority: ```php */ final class PriorityRangeAggregation extends AbstractRangeAggregation implements LocationAggregation { } ``` The `PriorityRangeAggregation` class extends `AbstractRangeAggregation`. The name of the class indicates that it aggregates the results by using the Range aggregation. An aggregation must implement the [`Ibexa\Contracts\Core\Repository\Values\Content\Query\Aggregation`](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-Core-Repository-Values-Content-Query-Aggregation.html) interface or inherit one of following abstract classes: - [`Ibexa\Contracts\Core\Repository\Values\Content\Query\Aggregation\AbstractRangeAggregation`](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-Core-Repository-Values-Content-Query-Aggregation-AbstractRangeAggregation.html) - [`Ibexa\Contracts\Core\Repository\Values\Content\Query\Aggregation\AbstractStatsAggregation`](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-Core-Repository-Values-Content-Query-Aggregation-AbstractStatsAggregation.html) - [`Ibexa\Contracts\Core\Repository\Values\Content\Query\Aggregation\AbstractTermAggregation`](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-Core-Repository-Values-Content-Query-Aggregation-AbstractTermAggregation.html) An aggregation can also implement one of the following interfaces: - [`Ibexa\Contracts\Core\Repository\Values\Content\Query\Aggregation\FieldAggregation`](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-Core-Repository-Values-Content-Query-Aggregation-FieldAggregation.html), based on content field - [`Ibexa\Contracts\Core\Repository\Values\Content\Query\Aggregation\LocationAggregation`](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-Core-Repository-Values-Content-Query-Aggregation-LocationAggregation.html), based on content location - [`Ibexa\Contracts\Core\Repository\Values\Content\Query\Aggregation\RawAggregation`](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-Core-Repository-Values-Content-Query-Aggregation-RawAggregation.html), based on details of the index structure > **Note: Aggregation definition** > > An aggregation definition must contain at least the name of an aggregation and optional aggregation parameters, such as, for example, the path (string) that is used to limit aggregation results to a specific subtree, content type identifier, or field definition identifier, which is mapped to the search index field name. > > Aggregation definition must be independent of the search engine used. A custom aggregation requires that the following elements are provided: - An aggregation visitor that returns an array of results - A result extractor that transforms raw aggregation results from the search engine into `AggregationResult` objects In simpler cases, you can apply one of the built-in visitors that correspond to the aggregation type. The example below uses `RangeAggregationVisitor`: **Solr** ```yaml services: app.search.solr.query.aggregation_visitor.priority_range_aggregation: class: Ibexa\Solr\Query\Common\AggregationVisitor\RangeAggregationVisitor factory: [ '@Ibexa\Solr\Query\Common\AggregationVisitor\Factory\SearchFieldAggregationVisitorFactory', 'createRangeAggregationVisitor' ] arguments: $aggregationClass: 'App\Query\Aggregation\Solr\PriorityRangeAggregation' $searchIndexFieldName: 'priority_i' tags: - { name: ibexa.search.solr.query.location.aggregation.visitor } ``` **Elasticsearch** ```yaml services: app.search.elasticsearch.query.aggregation_visitor.priority_range_aggregation: class: Ibexa\Elasticsearch\Query\AggregationVisitor\RangeAggregationVisitor factory: [ '@Ibexa\Elasticsearch\Query\AggregationVisitor\Factory\SearchFieldAggregationVisitorFactory', 'createRangeAggregationVisitor' ] arguments: $aggregationClass: 'App\Query\Aggregation\Elasticsearch\PriorityRangeAggregation' $searchIndexFieldName: 'priority_i' tags: - { name: ibexa.search.elasticsearch.query.location.aggregation.visitor } ``` The visitor is created by `SearchFieldAggregationVisitorFactory`. You provide it with two arguments: - The aggregation class in `aggregationClass` - The field name in search index in `searchIndexFieldName` In this example, the field is `priority_i` which exists only for locations. **Solr** Tag the service with `ibexa.search.solr.query.location.aggregation.visitor`. For content-based aggregations, use the `ibexa.search.solr.query.content.aggregation.visitor` tag. **Elasticsearch** Tag the service with `ibexa.search.elasticsearch.query.location.aggregation_visitor`. For content-based aggregations, use the `ibexa.search.elasticsearch.query.content.aggregation.visitor` tag. For the result extractor, you can use the built-in `RangeAggregationResultExtractor` and provide it with the aggregation class in the `aggregationClass` parameter. **Solr** Tag the service with `ibexa.search.solr.query.location.aggregation.result.extractor`. As `$keyMapper` to transform raw keys into more usable objects or scalar values, use `IntRangeAggregationKeyMapper` or create your own implementing [`RangeAggregationKeyMapper`](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-Solr-ResultExtractor-AggregationResultExtractor-RangeAggregationKeyMapper.html). ```yaml services: app.search.solr.query.aggregation_result_extractor.priority_range_aggregation: class: Ibexa\Solr\ResultExtractor\AggregationResultExtractor\RangeAggregationResultExtractor arguments: $aggregationClass: 'App\Query\Aggregation\Solr\PriorityRangeAggregation' $keyMapper: 'Ibexa\Solr\ResultExtractor\AggregationResultExtractor\RangeAggregationKeyMapper\IntRangeAggregationKeyMapper' tags: - { name: ibexa.search.solr.query.location.aggregation.result.extractor } ``` For other cases, a [`TermAggregationKeyMapper`](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-Solr-ResultExtractor-AggregationResultExtractor-TermAggregationKeyMapper.html) interface is also available. **Elasticsearch** Tag the service with `ibexa.search.elasticsearch.query.location.aggregation.result.extractor`. ```yaml services: app.search.elasticsearch.query.aggregation_result_extractor.priority_range_aggregation: class: Ibexa\Elasticsearch\Query\ResultExtractor\AggregationResultExtractor\RangeAggregationResultExtractor arguments: $aggregationClass: 'App\Query\Aggregation\Elasticsearch\PriorityRangeAggregation' tags: - { name: ibexa.search.elasticsearch.query.location.aggregation.result.extractor } ``` You can use a different type of aggregation, followed by respective visitor and extractor classes: **Solr** - Range - `Ibexa\Solr\Query\Common\AggregationVisitor\RangeAggregationVisitor` - `Ibexa\Solr\ResultExtractor\AggregationResultExtractor\RangeAggregationResultExtractor` - Stats (count, minimum, maximum, average, sum) - `Ibexa\Solr\Query\Common\AggregationVisitor\StatsAggregationVisitor` - `Ibexa\Solr\ResultExtractor\AggregationResultExtractor\StatsAggregationResultExtractor` - Term - `Ibexa\Solr\Query\Common\AggregationVisitor\TermAggregationVisitor` - `Ibexa\Solr\ResultExtractor\AggregationResultExtractor\TermAggregationResultExtractor` **Elasticsearch** - Range - `Ibexa\ElasticSearchEngine\Query\AggregationVisitor\RangeAggregationVisitor` - `Ibexa\ElasticSearchEngine\Query\ResultExtractor\AggregationResultExtractor\RangeAggregationResultExtractor` - Stats (count, minimum, maximum, average, sum) - `Ibexa\ElasticSearchEngine\Query\AggregationVisitor\StatsAggregationVisitor` - `Ibexa\ElasticSearchEngine\Query\ResultExtractor\AggregationResultExtractor\StatsAggregationResultExtractor` - Term - `Ibexa\ElasticSearchEngine\Query\AggregationVisitor\TermAggregationVisitor` - `Ibexa\ElasticSearchEngine\Query\ResultExtractor\AggregationResultExtractor\TermAggregationResultExtractor` In a more complex use case, you must create your own visitor and extractor. ### Create aggregation visitor **Solr** The aggregation visitor must implement [`Ibexa\Contracts\Solr\Query\AggregationVisitor`](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-Solr-Query-AggregationVisitor.html): ```php $aggregation */ public function visit( AggregationVisitor $dispatcherVisitor, Aggregation $aggregation, array $languageFilter ): array { $rangeFacets = []; foreach ($aggregation->getRanges() as $range) { $from = $this->formatRangeValue($range->getFrom()); $to = $this->formatRangeValue($range->getTo()); $rangeFacets["{$from}_{$to}"] = [ 'type' => 'query', 'q' => sprintf('priority_i:[%s TO %s}', $from, $to), ]; } return [ 'type' => 'query', 'q' => '*:*', 'facet' => $rangeFacets, ]; } private function formatRangeValue($value): string { if ($value === null) { return '*'; } return (string)$value; } } ``` **Elasticsearch** The aggregation visitor must implement [`Ibexa\Contracts\ElasticSearchEngine\Query\AggregationVisitor`](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-Elasticsearch-Query-AggregationVisitor.html): ```php $aggregation * * @return array> */ public function visit(AggregationVisitor $dispatcher, Aggregation $aggregation, LanguageFilter $languageFilter): array { $ranges = []; foreach ($aggregation->getRanges() as $range) { if ($range->getFrom() !== null && $range->getTo() !== null) { $ranges[] = [ 'from' => $range->getFrom(), 'to' => $range->getTo(), ]; } elseif ($range->getFrom() === null && $range->getTo() !== null) { $ranges[] = [ 'to' => $range->getTo(), ]; } elseif ($range->getFrom() !== null && $range->getTo() === null) { $ranges[] = [ 'from' => $range->getFrom(), ]; } else { // invalid range } } return [ 'range' => [ 'field' => 'priority_i', 'ranges' => $ranges, ], ]; } } ``` The `canVisit()` method checks whether the provided aggregation is of the supported type (in this case, your custom `PriorityRangeAggregation`). The `visit()` method returns an array of results. Finally, register the aggregation visitor as a service. **Solr** Tag the aggregation visitor with `ibexa.search.solr.query.location.aggregation.visitor`: ```yaml services: App\Query\Aggregation\Solr\PriorityRangeAggregationVisitor: tags: - { name: ibexa.search.solr.query.location.aggregation.visitor } ``` For content-based aggregations, use the `ibexa.search.solr.query.content.aggregation.visitor` tag. **Elasticsearch** Tag the aggregation visitor with `ibexa.elasticsearch.query.location.aggregation_visitor`: ```yaml services: App\Query\Aggregation\Elasticsearch\PriorityRangeAggregationVisitor: tags: - { name: ibexa.search.elasticsearch.query.location.aggregation.visitor } ``` For content-based aggregations, use the `ibexa.search.elasticsearch.query.content.aggregation.visitor` tag. ### Create result extractor **Solr** You must also create a result extractor, which implements [`Ibexa\Contracts\Solr\ResultExtractor\AggregationResultExtractor`](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-Solr-ResultExtractor-AggregationResultExtractor.html) that transforms raw aggregation results from Solr into `AggregationResult` objects: ```php $bucket) { if ($key === 'count' || !str_contains($key, '_')) { continue; } [$from, $to] = explode('_', $key, 2); $entries[] = new RangeAggregationResultEntry( new Range( $from !== '*' ? $from : null, $to !== '*' ? $to : null ), $bucket->count ); } return new RangeAggregationResult($aggregation->getName(), $entries); } } ``` The `canVisit()` method checks whether the provided aggregation is of the supported type (in this case, your custom `PriorityRangeAggregation`). The `extract()` method converts the [raw data provided by the search engine](https://solr.apache.org/guide/solr/9_8/query-guide/search-sample.html#aggregation) to a `RangeAggregationResult` object. **Elasticsearch** You must also create a result extractor, which implements [`Ibexa\Contracts\ElasticSearchEngine\Query\AggregationResultExtractor`](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-Elasticsearch-Query-AggregationResultExtractor.html) that transforms raw aggregation results from Elasticsearch into `AggregationResult` objects: ```php getName(), $entries); } } ``` The `supports()` method checks whether the provided aggregation is of the supported type (in this case, your custom `PriorityRangeAggregation`). The `extract()` method converts the [raw data provided by the search engine](https://www.elastic.co/docs/explore-analyze/query-filter/aggregations) to a `RangeAggregationResult` object. Finally, register the result extractor as a service. **Solr** Tag the result extractor with `ibexa.search.solr.query.location.aggregation.result.extractor`: ```yaml services: App\Query\Aggregation\Solr\PriorityRangeAggregationResultExtractor: tags: - { name: ibexa.search.solr.query.location.aggregation.result.extractor } ``` For content-based aggregations, use the `ibexa.search.solr.query.content.aggregation.result.extractor` tag. **Elasticsearch** Tag the result extractor with `ibexa.elasticsearch.query.location.aggregation_result_extractor`: ```yaml services: App\Query\Aggregation\Elasticsearch\PriorityRangeAggregationResultExtractor: tags: - { name: ibexa.search.elasticsearch.query.location.aggregation.result.extractor } ``` For content-based aggregations, use the `ibexa.search.elasticsearch.query.content.aggregation.result.extractor` tag. # Solr document field mappers > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). Use document field mappers to add additional data in Solr search engine. You can use document field mappers to index additional data in the search engine. The additional data can come from external sources (for example, the [Raptor recommendation connector](https://doc.ibexa.co/en/saas/recommendations/raptor_integration/raptor_connector/index.md), or from internal ones. An example of indexing internal data is indexing data through the location hierarchy: from the parent location to the child location, or indexing child data on the parent location. You can use this to find the content with full-text search, or to simplify a search in a complicated data model. To do this effectively, you must understand how the data is indexed with the Solr search engine. Solr uses [documents](https://solr.apache.org/guide/solr/9_8/getting-started/documents-fields-schema-design.html#how-solr-sees-the-world) as a unit of data that is indexed. Documents are indexed per translation, as content blocks. A block is a nested document structure. When used in Cohesivo, a parent document represents content, and locations are indexed as child documents of the content item. To avoid duplication, full-text data is indexed on the content document only. Knowing this, you can index additional data by the following: - All block documents (meaning content and its locations, all translations) - All block documents per translation - Content documents - Content documents per translation - Location documents Additional data is indexed by implementing a document field mapper and registering it at one of the five extension points described above. You can create the field mapper class anywhere inside your bundle, as long as you register it as a Symfony service. There are three different field mappers. Each mapper implements two methods, by the same name, but accepting different arguments: - [`ContentFieldMapper`](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-Solr-FieldMapper-ContentTranslationFieldMapper.html) - [`::accept(Content $content)`](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-Solr-FieldMapper-ContentTranslationFieldMapper.html#method_accept) - [`::mapFields(Content $content)`](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-Solr-FieldMapper-ContentTranslationFieldMapper.html#method_mapFields) - [`ContentTranslationFieldMapper`](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-Solr-FieldMapper-ContentTranslationFieldMapper.html) - [`::accept(Content $content, $languageCode)`](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-Solr-FieldMapper-ContentTranslationFieldMapper.html#method_accept) - [`::mapFields(Content $content, $languageCode)`](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-Solr-FieldMapper-ContentTranslationFieldMapper.html#method_mapFields) - [`LocationFieldMapper`](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-Solr-FieldMapper-LocationFieldMapper.html) - [`::accept(Location $content)`](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-Solr-FieldMapper-LocationFieldMapper.html#method_accept) - [`::mapFields(Location $content)`](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-Solr-FieldMapper-LocationFieldMapper.html#method_mapFields) Mappers can be used on the extension points by registering them with the [service container](https://doc.ibexa.co/en/saas/api/php_api/php_api/#service-container) by using service tags, as follows: - All block documents - `ibexa.search.solr.field.mapper.block` - All block documents per translation - `ibexa.search.solr.field.mapper.block.translation` - Content documents - `ibexa.search.solr.field.mapper.content` - Content documents per translation - `ibexa.search.solr.field.mapper.content.translation` - Location documents - `ibexa.search.solr.field.mapper.location` The following example shows how you can index data from the parent location content, to make it available for search on the child content. The example relies on a use case of indexing webinar data on the webinar events, which are children of the webinar. The field mapper could then look like this: ```php versionInfo->contentInfo->contentTypeId === 42; } /** * @return \Ibexa\Contracts\Core\Search\Field[] */ public function mapFields(Content $content): array { $mainLocationId = $content->versionInfo->contentInfo->mainLocationId; $location = $this->locationHandler->load($mainLocationId); $parentLocation = $this->locationHandler->load($location->parentId); $parentContentInfo = $this->contentHandler->loadContentInfo($parentLocation->contentId); return [ new Search\Field( 'parent_name', $parentContentInfo->name, new Search\FieldType\StringField() ), ]; } } ``` You index text data only on the content document, therefore, you would register the service like this: ```yaml services: App\Search\FieldMapper\WebinarEventParentNameFieldMapper: arguments: - '@Ibexa\Contracts\Core\Persistence\Content\Handler' - '@Ibexa\Contracts\Core\Persistence\Content\Location\Handler' tags: - {name: ibexa.search.solr.field.mapper.content} ``` > **Caution: Permission issues when using Repository API in document field mappers** > > Document field mappers are low-level and expect to be able to index all content regardless of current user permissions. If you use PHP API in your custom document field mappers, apply [`sudo()`](https://doc.ibexa.co/en/saas/api/php_api/php_api/#using-sudo), or use the Persistence SPI layer as in the example above. # Index custom Elasticsearch data > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). Index custom data when using the Elasticsearch search engine. [Elasticsearch](https://doc.ibexa.co/en/saas/search/search_engines/elasticsearch/elasticsearch_overview/index.md) indexes content and location data out of the box. Besides what is indexed automatically, you can add additional data to the Elasticsearch index. To do so, subscribe to one of the following events: - [`Ibexa\Contracts\ElasticSearchEngine\Mapping\Event\ContentIndexCreateEvent`](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-Elasticsearch-Mapping-Event-ContentIndexCreateEvent.html) - [`Ibexa\Contracts\ElasticSearchEngine\Mapping\Event\LocationIndexCreateEvent`](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-Elasticsearch-Mapping-Event-LocationIndexCreateEvent.html) These events are called when the index is created for the content and location documents. You can pass the event to a subscriber which gives you access to the document that you can modify. In the following example, when an index in created for a content or a location document, the event subscriber adds a `custom_field` of the type [`StringField`](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-Core-Search-FieldType-StringField.html) to the index: ```php getDocument(); $document->fields[] = new Field( 'custom_field', 'Custom field value', new StringField() ); } public function onLocationDocumentCreate(LocationIndexCreateEvent $event): void { $document = $event->getDocument(); $document->fields[] = new Field( 'custom_field', 'Custom field value', new StringField() ); } public static function getSubscribedEvents(): array { return [ ContentIndexCreateEvent::class => 'onContentDocumentCreate', LocationIndexCreateEvent::class => 'onLocationDocumentCreate', ]; } } ``` If you're not using [Symfony's autoconfiguration](https://symfony.com/doc/7.4/service_container.html#the-autoconfigure-option) for event subscribers, register it as a service: ```yaml services: App\EventSubscriber\CustomIndexDataSubscriber: tags: - { name: kernel.event_subscriber } ``` # Customize Elasticsearch index structure > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). You can adapt the structure of Elasticsearch index to the data in your Repository to improve performance and avoid instability. You can customize the structure of your Elasticsearch search index to manage how documents in the index are grouped. This lets you control the size of [Elasticsearch shards](https://www.elastic.co/docs/deploy-manage/production-guidance/scaling-considerations) that the index is divided into. By customizing the structure to your needs, you can avoid "oversharding" (having too many shards), which negatively affects performance and can lead to instability. For more information about adapting the size of your search index shards, see [Elasticsearch documentation](https://www.elastic.co/guide/en/elasticsearch/reference/8.4/size-your-shards.html). ## Selecting indexing strategy In your Elasticsearch configuration you can select one of four built-in strategies that control grouping documents in the index. The strategies are: - `NullGroupResolver` - groups all documents into a single group. - `LanguageGroupResolver` - groups documents by language code. - `ContentTypeGroupResolver`- groups documents by content type ID. - `CompositeGroupResolver` - allows combining multiple group resolves together to have a more granular index (default). The default strategy is the composite of language and content type ID, resulting in indexes in the form of `___`. To change the strategy, use the `ibexa_elasticsearch.document_group_resolver` [configuration key](https://doc.ibexa.co/en/saas/administration/configuration/configuration/#configuration-files): ```yaml ibexa_elasticsearch: document_group_resolver: 'Ibexa\Elasticsearch\ElasticSearch\Index\Group\ContentTypeGroupResolver' ``` Select the strategy based on the structure of your repository, taking into accounts data such as the number of content items, content types, or languages. ## Custom indexing strategy You can also create a group resolver that provides a custom indexing strategy. This resolver must implement `Ibexa\Contracts\Elasticsearch\ElasticSearch\Index\Group\GroupResolverInterface`. ### Create group resolver In this example, create a `ContentTypeGroupGroupResolver` based on the content type Group ID of the document: ```php contentTypeHandler->load($document->contentTypeId)->groupIds[0]; return (string)$index; } } ``` Register the resolver as a service: ```yaml services: App\GroupResolver\ContentTypeGroupGroupResolver: arguments: $contentTypeHandler: '@Ibexa\Contracts\Core\Persistence\Content\Type\Handler' ``` ### Configure indexing strategy Finally, in configuration indicate that Elasticsearch should use your custom indexing strategy: ```yaml ibexa_elasticsearch: document_group_resolver: 'App\GroupResolver\ContentTypeGroupGroupResolver' ``` # Manipulate Elasticsearch query > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). Manipulate the search query when using the Elasticsearch search engine. You can customize the search query before it's executed. To do it, subscribe to [`Ibexa\Contracts\Elasticsearch\Query\Event\QueryFilterEvent`](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-Elasticsearch-Query-Event-QueryFilterEvent.html). The following example shows how to add a Search Criterion to all queries. Depending on your configuration, this might impact all search queries, including those used for search and content tree in the back office. ```php getQuery(); $additionalCriteria = new ObjectStateIdentifier('locked'); if ($query->filter !== null) { $query->filter = $additionalCriteria; } else { // Append Criterion to existing filter $query->filter = new LogicalAnd([ $query->filter, $additionalCriteria, ]); } } public static function getSubscribedEvents(): array { return [ QueryFilterEvent::class => 'onQueryFilter', ]; } } ``` If you're not using [Symfony's autoconfiguration](https://symfony.com/doc/7.4/service_container.html#the-autoconfigure-option) for event subscribers, register it as a service: ```yaml services: App\EventSubscriber\CustomQueryFilterSubscriber: tags: - { name: kernel.event_subscriber } ``` # Reindex search > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). Reindexing lets you create or refresh the search engine index. To (re)create or refresh the search engine index for configured search engines (per SiteAccess repository), use the `php bin/console ibexa:reindex` command. Some examples of common usage: ```bash # Reindex the whole index using parallel process (by default starts by purging the whole index) # (with the 'auto' option which detects the number of CPU cores -1, default behavior) php bin/console ibexa:reindex --processes=auto # Refresh a part of the subtree (implies --no-purge) php bin/console ibexa:reindex --subtree=2 # Refresh content updated since a date (implies --no-purge) php bin/console ibexa:reindex --since=yesterday # Refresh (or delete when not found) content by IDs (implies --no-purge) php bin/console ibexa:reindex --content-ids=3,45,33 ``` For more information, see `php bin/console ibexa:reindex --help`. # Infrastructure and maintenance # Infrastructure and maintenance > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). Learn how to handle cache, use a clustering setup, configure databases and ensure your installation is performing well. - [Cache](https://ez-systems-developer-documentation--3381.com.readthedocs.build/en/3381/infrastructure_and_maintenance/cache/cache/): For caching, Cohesivo offers both HTTP cache for content views, and persistence cache. - [Clustering](https://ez-systems-developer-documentation--3381.com.readthedocs.build/en/3381/infrastructure_and_maintenance/clustering/clustering/): Clustering enables you to host one installation of Cohesivo on multiple servers. - [Performance](https://ez-systems-developer-documentation--3381.com.readthedocs.build/en/3381/infrastructure_and_maintenance/performance/): Ensure that your Cohesivo installation performs well by following our set of recommendations. - [Background tasks](https://ez-systems-developer-documentation--3381.com.readthedocs.build/en/3381/infrastructure_and_maintenance/background_tasks/): Use Ibexa Messenger to run processes in the background and conserve system resources. - [Databases](https://ez-systems-developer-documentation--3381.com.readthedocs.build/en/3381/infrastructure_and_maintenance/databases/): Cohesivo can use MySQL, PostgreSQL or MariaDB as its database. - [Environments](https://ez-systems-developer-documentation--3381.com.readthedocs.build/en/3381/infrastructure_and_maintenance/environments/): In Cohesivo you can use environment provided by Symfony in virtual host configuration, and to create custom environments. - [Support and maintenance FAQ](https://ez-systems-developer-documentation--3381.com.readthedocs.build/en/3381/infrastructure_and_maintenance/support_and_maintenance_faq/): See how you can resolve common issues and report a Customer Support ticket. # Request lifecycle: from request to response > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). See the lifecycle of an HTTP request in Cohesivo, from request to response. ## Beginning of HTTP request When entering the server infrastructure, the HTTP request can be handled by several component such as a firewall, a load balancer, or a reverse proxy before arriving on the web server itself. For an overview of what happens on a reverse proxy like Varnish or Fastly, see [Context-aware HTTP cache / Request lifecycle](https://doc.ibexa.co/en/saas/infrastructure_and_maintenance/cache/http_cache/context_aware_cache/#request-lifecycle). When arriving at a web server, the request is filtered by Apache virtual host, Nginx Server Blocks, or equivalent. There, requests of static resources are separated from requests to PHP interpreter. As Cohesivo is a Symfony application, the handling of requests starts like in Symfony (see [Symfony and HTTP Fundamentals](https://symfony.com/doc/7.4/introduction/http_fundamentals.html)). If the HTTP request is to be treated by Cohesivo, it goes to the `public/index.php` of the [Symfony Front Controller](https://symfony.com/doc/7.4/configuration/front_controllers_and_kernel.html#the-front-controller). The front controller transforms the HTTP request into a PHP [`Request` object](https://symfony.com/doc/7.4/introduction/http_fundamentals.html#symfony-request-object) and passes it to Symfony's Kernel to get a [`Response` object](https://symfony.com/doc/7.4/introduction/http_fundamentals.html#symfony-response-object) that is transformed and sent back as an HTTP response. The schemas start with a regular `Request` object from a browser that enters Symfony and Cohesivo. There is no ESI, no REST, and no GraphQL request performed. ## Lifecycle flowcharts ### Concept flowchart The chart below introduces the logic of the request treatment. ![Simplified request lifecycle flowchart](https://doc.ibexa.co/en/saas/infrastructure_and_maintenance/img/request_lifecycle_concept.png) ### Kernel events flowchart The following chart shows events, listeners and attributes added to the request or its wrapping event object. ![Detailed request lifecycle flowchart organised around kernel events](https://doc.ibexa.co/en/saas/infrastructure_and_maintenance/img/request_lifecycle_events.png) This schema is described below event by event. > **Tip: Tip** > > To list all listeners that listen to an event, run `php bin/console debug:event-dispatcher `, for example: > > ```bash > php bin/console debug:event-dispatcher kernel.request > ``` > > To view details of a service (including class, arguments and tags), run `php bin/console debug:container --show-arguments `, for example: > > ```bash > php bin/console debug:container --show-arguments ibexa.siteaccess_match_listener` > ``` > > To list all services with a specific tag, run `php bin/console debug:container --tag=`, for example: > > ```bash > php bin/console debug:container --tag=router > ``` ## Kernel's request event When the request enters the Symfony's kernel (and goes underneath the [`HttpKernel`](https://symfony.com/doc/7.4/components/http_kernel.html), `http_kernel`), a `kernel.request` event (`KernelEvents::REQUEST`) is dispatched. Several listeners are called in decreasing priority. ### SiteAccess matching The [`FragmentListener`](https://github.com/symfony/http-kernel/blob/5.3/EventListener/FragmentListener.php) (priority 48) handles the request first, and then it passes to the `ibexa.siteaccess_match_listener` service (priority 45). This service can be either: - purely the `SiteAccessMatchListener` or - its `UserContextSiteAccessMatchSubscriber` decoration when HTTP cache is used. The `ibexa.siteaccess_match_listener` service: - finds the current SiteAccess using the `SiteAccess\Router` (`Ibexa\Core\MVC\Symfony\SiteAccess\Router`) regarding the [SiteAccess Matching configurations](https://doc.ibexa.co/en/saas/multisite/siteaccess/siteaccess_matching/index.md), - adds the current SiteAccess to the `Request` object's **`siteaccess`** attribute, - then dispatches the `Ibexa\Core\MVC\Symfony\SiteAccess` event (`MVCEvents::SITEACCESS`). The `SiteAccessListener` (`Ibexa\Bundle\Core\EventListener\SiteAccessListener`) subscribes to this `Ibexa\Core\MVC\Symfony\SiteAccess` event with top priority (priority 255). The `SiteAccessListener` adds the **`semanticPathinfo`** attribute, the path without SiteAccess indications ([`URIElement`](https://doc.ibexa.co/en/saas/multisite/siteaccess/siteaccess_matching/#urielement), [`URIText`](https://doc.ibexa.co/en/saas/multisite/siteaccess/siteaccess_matching/#uritext), or [`Map\URI`](https://doc.ibexa.co/en/saas/multisite/siteaccess/siteaccess_matching/#mapuri) implementing the `URILexer` interface) to the request. ### Routing Finally, the `Symfony\Component\HttpKernel\EventListener\RouterListener` (`router_listener`) (priority 32), which also listens to the `kernel.request` event, calls `Ibexa\Core\MVC\Symfony\Routing\ChainRouter::matchRequest` and adds its returned parameters to the request. #### `ChainRouter` The [`ChainRouter`](https://symfony.com/bundles/CMFRoutingBundle/current/routing-component/chain.html) is a Symfony Content Management Framework (CMF) component. Cohesivo makes it a service named `Ibexa\Core\MVC\Symfony\Routing\ChainRouter`. It has a collection of prioritized routers where to find one matching the request. The `ChainRouter` router collection is built by the `ChainRoutingPass`, collecting the services tagged `router`. The `DefaultRouter` is always added to the collection with top priority (priority 255). #### `DefaultRouter` `DefaultRouter` (`router.default`): The `DefaultRouter` tries to match the `semanticPathinfo` against routes, close to [the way pure Symfony does](https://symfony.com/doc/7.4/routing.html), by extending and using `Symfony\Component\Routing\Router`. If a route matches, the controller associated with it is responsible for building a `View` or `Response` object. ### `UrlWildcardRouter` `UrlWildcardRouter` (`ezpublish.urlwildcard_router`): If [URL Wildcards](https://doc.ibexa.co/en/saas/content_management/url_management/url_management/#url-wildcards) have been enabled, then the `URLWildcardRouter` is the next router tried. If a wildcard matches, the request's `semanticPathinfo` is updated and the router throws a `ResourceNotFoundException` to continue with the `ChainRouter` collection's next entry. ### `UrlAliasRouter` `UrlAliasRouter` (`Ibexa\Bundle\Core\Routing\UrlAliasRouter`): This router uses the `UrlAliasService` to associate the `semanticPathinfo` to a location. If it finds a location, the request receives the attributes **`locationId`** and **`contentId`**, **`viewType`** is set to `full`, and the **`_controller`** is set to `ibexa_content::viewAction` for now. The `locale_listener` (priority 16) sets the request's **`_locale`** attribute. > **Note: Permission control** > > Another `kernel.request` event listener is the `Ibexa\AdminUi\EventListener\RequestListener` (priority 13). When a route gets a `siteaccess_group_whitelist` parameter, this listener checks that the current SiteAccess is in one of the listed groups. For example, the back office sets an early protection of its routes by passing them a `siteaccess_group_whitelist` containing only the `admin_group`. Now, when the `Request` knows its controller, the `HttpKernel` dispatches the `kernel.controller` event. ## Kernel's controller event ### View building and matching When HttpKernel dispatches the `kernel.controller` event, the following things happen. Listening to `kernel.controller`, the `ViewControllerListener` (`Ibexa\Bundle\Core\EventListener\ViewControllerListener`) (priority 10) checks if the `_controller` request attribute is associated with a `ViewBuilder` (a service tagged `ibexa.view.builder`) in the `ViewBuilderRegistry` (`Ibexa\Core\MVC\Symfony\View\Builder\Registry\ControllerMatch`). The `ContentViewBuilder` (`Ibexa\Core\MVC\Symfony\View\Builder\ContentViewBuilder`) matches on controller starting with `ibexa_content:` (see `Ibexa\Core\MVC\Symfony\View\Builder\ContentViewBuilder::matches`). The `ContentViewBuilder` builds a `ContentView`. First, the `ContentViewBuilder` loads the `Location` and the `Content`, and adds them to the `ContentView` object. > **Caution: Permission control** > > `content/read` and/or `content/view_embed` permissions are controlled during this `ContentView` building. Then, the `ContentViewBuilder` passes the `ContentView` to its `View\Configurator` (`Ibexa\Core\MVC\Symfony\View\Configurator\ViewProvider`). It's implemented by the `View\Configurator\ViewProvider` and its `View\Provider\Registry`. This registry receives the services tagged `ibexa.view.provider` thanks to the `ViewProviderPass`. Among the view providers, the services using the `Ibexa\Bundle\Core\View\Provider\Configured` have an implementation of the `MatcherFactoryInterface` (`ibexa.content_view.matcher_factory`). Through service decoration and class inheritance, the `ClassNameMatcherFactory` is responsible for the [view matching](https://doc.ibexa.co/en/saas/templating/templates/template_configuration/#view-rules-and-matching). The `View\Configurator\ViewProvider` uses the matched view rule to add possible **`templateIdentifier`** and **`controllerReference`** to the `ContentView` object. The `ViewControllerListener` adds the ContentView to the `Request` as the **`view`** attribute. The `ViewControllerListener` eventually updates the request's `_controller` attribute with the `ContentView`'s `controllerReference`. The `HttpKernel` then dispatches a `kernel.controller_arguments` (`KernelEvents::CONTROLLER_ARGUMENTS`) but nothing from Cohesivo is listening to it. ## Controller execution The `HttpKernel` extracts from the request the controller and the arguments to pass to the controller. [Argument resolvers](https://symfony.com/doc/7.4/controller/value_resolver.html) work in a way similar to autowiring. The `HttpKernel` executes the controller with those arguments. As a reminder, the controller and its argument can be: - A controller set by the matched route and the request as its argument. - The default `ibexa_content::viewAction` controller and a `ContentView` as its argument. - A [custom controller](https://doc.ibexa.co/en/saas/templating/queries_and_controllers/controllers/index.md) set by the matched view rule and a `View` or the request as its argument (most likely a `ContentView` but there is no restriction). > **Caution: Permission control** > > See [Permissions for custom controller](https://doc.ibexa.co/en/saas/permissions/permission_overview/#permissions-for-custom-controllers). ## Kernel's view event and `ContentView` rendering If the controller returns something other than `Response`, the `HttpKernel` dispatches a `kernel.view` event (`KernelEvents::VIEW`). In the case of a URL Alias, the controller most likely returns a ContentView. The `ViewRendererListener` (`Ibexa\Bundle\Core\EventListener\ViewRendererListener`) uses the `ContentView` and the `TemplateRenderer` (`Ibexa\Core\MVC\Symfony\View\Renderer\TemplateRenderer`) to get the content of the `Response` and attach this new `Response` to the event. The `HttpKernel` retrieves the response attached to the event and continues. ## Summary ### Summary of events and services - event=`kernel.request` - 45:`ibexa.siteaccess_match_listener` - `Ibexa\Core\MVC\Symfony\SiteAccess\Router` - event=`Ibexa\Core\MVC\Symfony\SiteAccess` - 255:`Ibexa\Bundle\Core\EventListener\SiteAccessListener` - 32:`router_listener` - `Ibexa\Core\MVC\Symfony\Routing\ChainRouter` - tag=`router` - `router.default` - `ezpublish.urlwildcard_router` - `Ibexa\Bundle\Core\Routing\UrlAliasRouter` - 16:`locale_listener` - 13:`Ibexa\AdminUi\EventListener\RequestListener` - event=`kernel.controller` - 10:`Ibexa\Bundle\Core\EventListener\ViewControllerListener` - `Ibexa\Core\MVC\Symfony\View\Builder\Registry\ControllerMatch` - tag=`ibexa.view.builder` - `Ibexa\Core\MVC\Symfony\View\Builder\ContentViewBuilder` - `Ibexa\Core\MVC\Symfony\View\Configurator\ViewProvider` - event=`kernel.controller_arguments` - event=`kernel.view` - 0:`Ibexa\Bundle\Core\EventListener\ViewRendererListener` - `Ibexa\Core\MVC\Symfony\View\Renderer\TemplateRenderer` - event=`kernel.response` - event=`kernel.terminate` - 0:`Ibexa\Bundle\Core\EventListener\BackgroundIndexingTerminateListener` ### Examples request attributes timeline | Event | Service | Request attribute | Example | | ------------------------------------- | ------------------------------------------------------------ | ------------------------ | ----------------------------------------- | | | http_kernel | pathInfo | /en/about | | kernel.request | ibexa.siteaccess_match_listener | siteaccess | en | | Ibexa\\Core\\MVC\\Symfony\\SiteAccess | Ibexa\\Bundle\\Core\\EventListener\\SiteAccessListener | semanticPathinfo | /about | | kernel.request | router.default | \_route | N/A | | kernel.request | router.default | \_controller | N/A | | kernel.request | Ibexa\\Bundle\\Core\\Routing\\UrlAliasRouter | \_route | ibexa.url.alias | | kernel.request | Ibexa\\Bundle\\Core\\Routing\\UrlAliasRouter | \_controller | \*\*ibexa_content::\*\*viewAction | | kernel.request | Ibexa\\Bundle\\Core\\Routing\\UrlAliasRouter | viewType | full | | kernel.request | Ibexa\\Bundle\\Core\\Routing\\UrlAliasRouter | contentId | 1 | | kernel.request | Ibexa\\Bundle\\Core\\Routing\\UrlAliasRouter | locationId | 42 | | kernel.request | locale_listener | \_locale | en_GB | | kernel.controller | Ibexa\\Core\\MVC\\Symfony\\View\\Builder\\ContentViewBuilder | view.content | Content | | kernel.controller | Ibexa\\Core\\MVC\\Symfony\\View\\Builder\\ContentViewBuilder | view.location | Location | | kernel.controller | Ibexa\\Core\\MVC\\Symfony\\View\\Configurator\\ViewProvider | view.templateIdentifier | @IbexaCore/default/content/full.html.twig | | kernel.controller | Ibexa\\Core\\MVC\\Symfony\\View\\Configurator\\ViewProvider | view.controllerReference | null | | kernel.controller | Ibexa\\Bundle\\Core\\EventListener\\ViewControllerListener | view | ContentView | | kernel.controller | Ibexa\\Bundle\\Core\\EventListener\\ViewControllerListener | \_controller | ibexa_content::viewAction | | (controller execution) | http_kernel | | ContentView | | kernel.view | Ibexa\\Bundle\\Core\\EventListener\\ViewRendererListener | response | Response | ## End of HTTP response The web server outputs the HTTP response. Depending on the architecture, few things may still occur. For example, Varnish or Fastly can take specific headers into account when setting the cache or serving it. # Databases > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). Cohesivo can use MySQL, PostgreSQL or MariaDB as its database. ## Using PostgreSQL Cohesivo uses MySQL by default, but you can also choose to install it with PostgreSQL. ### Requirements To use PostgreSQL, you need to have the `pdo_pgsql` PHP extension installed. ### Provide parameters When you run `composer install`, you're asked to provide installation parameters. > **Tip: Tip** > > It's recommended to store the database credentials in your `.env.local` file and not commit it to the Version Control System. If you use PostgreSQL, the following parameters need to be set differently in the `.env.local` file than when using MySQL: - `DATABASE_NAME` - `DATABASE_HOST` - `DATABASE_PORT` - `DATABASE_PLATFORM` must be set to `pgsql` instead of `mysql` - `DATABASE_DRIVER` must be set to `pdo_pgsql` instead of the default `pdo_mysql` - `DATABASE_VERSION` - `DATABASE_CHARSET` must be set to `utf8`, because the default value of `utf8mb4` is MySQL-specific. The rest of the installation procedure is the same as when using MySQL. # Cache > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). For caching, Cohesivo offers both HTTP cache for content views, and persistence cache. Cohesivo offers both HTTP cache for content views, and persistence cache. - [HTTP cache](https://ez-systems-developer-documentation--3381.com.readthedocs.build/en/3381/infrastructure_and_maintenance/cache/http_cache/http_cache/): Cohesivo's HTTP cache functionalities enable using reverse proxies - Symfony HttpCache Proxy, Varnish or Fastly. - [Reverse proxy](https://ez-systems-developer-documentation--3381.com.readthedocs.build/en/3381/infrastructure_and_maintenance/cache/http_cache/reverse_proxy/): You can use Symfony HttpCache Proxy, Varnish or Fastly as reverse proxies with Cohesivo. - [Persistence cache](https://ez-systems-developer-documentation--3381.com.readthedocs.build/en/3381/infrastructure_and_maintenance/cache/persistence_cache/): Persistence cache caches SPI\\Persistence calls used in common page loads. # HTTP cache > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). Cohesivo's HTTP cache functionalities enable using reverse proxies - Symfony HttpCache Proxy, Varnish or Fastly. Cohesivo provides advanced caching features needed for its own content views, to make Varnish and Fastly act as the view cache for the system. This and other features allow Cohesivo to be scaled up to serve high traffic websites and applications. HTTP cache is handled by the [ibexa/http-cache](https://github.com/ibexa/http-cache) bundle, which extends [friendsofsymfony/http-cache-bundle](https://foshttpcachebundle.readthedocs.io/en/latest/), a Symfony community bundle that in turn extends [Symfony HTTP cache](https://symfony.com/doc/7.4/http_cache.html). For content view responses coming from Cohesivo itself, this means that: - Cache is **[content-aware](https://doc.ibexa.co/en/saas/infrastructure_and_maintenance/cache/http_cache/content_aware_cache/index.md)**, always kept up-to-date by invalidating using cache tags. - Cache is **[context-aware](https://doc.ibexa.co/en/saas/infrastructure_and_maintenance/cache/http_cache/context_aware_cache/index.md)**, to cache request for logged-in users by varying on user permissions. All of this works across all the supported reverse proxies: - [Symfony HttpCache Proxy](https://doc.ibexa.co/en/saas/infrastructure_and_maintenance/cache/http_cache/reverse_proxy/index.md) - limited to a single server, and with limited performance/features - [Varnish](https://www.varnish.org/) - high performance reverse proxy - [Fastly](https://www.fastly.com/) - Varnish-based CDN service You can use all these features in custom controllers as well. # HTTP cache configuration > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). Configure HTTP cache for Cohesivo, including cache header rules and time-to-live. HTTP cache configuration is SiteAccess-aware. ## Content view configuration You can configure cache globally for content views under the `ibexa.system..content` [configuration key](https://doc.ibexa.co/en/saas/administration/configuration/configuration/#configuration-files): ```yaml ibexa: system: : content: # Activates HTTP cache for content view_cache: true # Activates expiration based HTTP cache for content (very fast) ttl_cache: true # Number of seconds an HTTP response cache is valid (if ttl_cache is true, and if no custom s-maxage is set) default_ttl: 7200 ``` You may want to set a high default time to live (TTL) (`default_ttl`) to have a high cache hit ratio on your installation. As the system takes care of purges, the cache should not become stale with the exception of grace handing in Varnish and Fastly. ## Cache header rules A few redirect and error pages are served through the content view system. If you set a high `default_ttl`, they can also be served from cache. To avoid this, the installation ships with configuration to match these specific situations and set a much lower TTL. [FOSHttpCacheBundle matching rules](https://foshttpcachebundle.readthedocs.io/en/latest/reference/configuration/headers.html) enables you to specify a different TTL: ```yaml fos_http_cache: cache_control: rules: # Make sure cacheable (fresh) responses from Cohesivo which are errors/redirects get lower TTL than default_ttl - match: match_response: 'response.isFresh() && ( response.isServerError() || response.isClientError() || response.isRedirect() )' headers: overwrite: true cache_control: max_age: 5 s_maxage: 20 ``` Similarly, by default the performance tuning is applied to avoid crawlers affecting the setup too much, by caching of generic 404s and similar error pages in the following way: ```yaml fos_http_cache: cache_control: rules: # Example of performance tuning, force TTL on 404 pages to avoid crawlers, etc., taking too much load # Should not be set too high, as cached 404s can cause issues for future routes, URL aliases, wildcards, etc. - match: match_response: '!response.isFresh() && response.isNotFound()' headers: overwrite: true cache_control: public: true max_age: 0 s_maxage: 20 ``` ## Time-to-live value for Page blocks For the Page Builder, block cache by default respects `$content.ttl_cache$` and `$content.default_ttl$` settings. However, if the given block value has a since or till date, it's taken into account for the TTL calculation for both the block and the whole page. To overload this behavior, listen to [`BlockResponseEvents::BLOCK_RESPONSE`](https://doc.ibexa.co/en/saas/api/event_reference/page_events/index.md), and set priority to `-200` to adapt what Page field type does by default. For example, to disable cache for the block, use `$event->getResponse()->setPrivate()`. ## When to use ESI [Edge Side Includes](https://symfony.com/doc/7.4/http_cache/esi.html) (ESI) can be used to split out the different parts of a web page into separate fragments that can be freely reused as pieces by reverse proxy. In practice, with ESI, every sub-request is regenerated from application perspective. And while you can tune your system to reduce this, it always causes additional overhead in the following situations: - When cache is cold on all or some of the sub-requests - With Symfony Proxy (AppCache) there is always some overhead, even on warm cache (hits) - In development environment This may differ depending on your system, however, it's recommended to stay below 5 ESI requests per page and only using them for parts that are the same across the whole site or larger parts of it. You should not use ESI for parts that are effectively uncached, because your reverse proxy has to wait for the back end and cannot deliver cached pages directly. > **Note: ESI limitations with the URIElement SiteAccess matcher** > > It isn't possible to share ESIs across the SiteAccesses when using URI matching as URI contains the SiteAccess name encoded in its path information. # Reverse proxy > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). You can use Symfony HttpCache Proxy, Varnish or Fastly as reverse proxies with Cohesivo. ## Using Symfony reverse proxy To use the Symfony reverse proxy, you must change your `public/index.php` front controller script and wrap `Ibexa\Bundle\HttpCache\AppCache` instead of `Symfony\Bundle\FrameworkBundle\HttpCache\HttpCache` around the kernel. ```diff --- a/public/index.php +++ b/public/index.php @@ -1,9 +1,11 @@ **Caution: Caution** > > Don't enable the Symfony reverse proxy in `public/index.php` if you intend to use Varnish or Fastly. You may only use one HTTP cache at a time. ## Using Varnish or Fastly As Cohesivo is built on top of Symfony, it uses standard HTTP cache headers. By default, the Symfony reverse proxy is used to handle cache. You can replace it with other reverse proxies, such as Varnish, or CDN like Fastly. Using a different proxy is highly recommended as they provide better performance and more advanced features such as grace handling, configurable logic through VCL and much more. > **Note: Note** > > Use of Varnish or Fastly is a requirement for a [clustering](https://doc.ibexa.co/en/saas/infrastructure_and_maintenance/clustering/clustering/index.md) setup, as Symfony Proxy doesn't support sharing cache between several application servers. ## VCL base files For reverse proxies to work properly with your installation, you need to add the corresponding VCL files for your HTTP Cache. - Varnish config can be found in `vendor/ibexa/http-cache/docs/varnish/vcl`: - use [parameters.vcl](https://github.com/ibexa/http-cache/blob/6.0/docs/varnish/vcl/parameters.vcl) for installation specific settings - plus one of the `varnish*.vcl` corresponding to your Varnish version - For example, [varnish7.vcl](https://github.com/ibexa/http-cache/blob/6.0/docs/varnish/vcl/varnish7.vcl) when using Varnish 7 - Fastly config can be found in `vendor/ibexa/fastly/fastly`. You must install the following to use Fastly: - `ibexa_main.vcl` as the **main** custom VCL - `ibexa_user_hash.vcl` as another custom VCL - `snippet_re_enable_shielding.vcl` as snippet The provided `.vcl` files work both with [Fastly Shielding](https://www.fastly.com/documentation/guides/getting-started/hosts/shielding/) enabled and without it. If you decide to use Fastly VCL, consider using [Fastly CLI](https://www.fastly.com/documentation/reference/tools/cli/#installing) with it to manage VCL files from the command line. To learn more, see [Prepare to use Fastly locally](https://doc.ibexa.co/en/saas/infrastructure_and_maintenance/cache/http_cache/fastly/#prepare-for-using-fastly-locally) and [Introduction to Fastly CLI](https://doc.ibexa.co/en/saas/infrastructure_and_maintenance/cache/http_cache/fastly/#quick-introduction-to-fastly-cli). > **Tip: Support for Fastly Shielding was added in Cohesivo v3.3.24 and v4.1.6** > > When you extend [FOSHttpCacheBundle](https://foshttpcachebundle.readthedocs.io/en/latest/), you can also adapt your VCL further with [FOSHttpCache documentation](https://foshttpcache.readthedocs.io/en/latest/varnish-configuration.html) to use additional features. ## Configure Varnish and Fastly The configuration of Cohesivo for using Varnish or Fastly requires a few steps, starting with configuring proxy. Failing to configure reverse proxies correctly may introduce several problems, including, but not limited to: - Cohesivo generating links with a wrong protocol schema (HTTP instead of HTTPS) if HTTPS termination is done before the web server due to the `X-Forward-Proto` headers being ignored - Cohesivo generating links with wrong port numbers due to the `X-Forward-Port` headers being ignored - back office showing the login screen because JWT tokens aren't accepted due to the `X-Forward-For` headers being ignored ### Configure Symfony front controller You need to consider your `TrustedProxy` configuration when you use Symfony [behind a load balancer or a reverse proxy](https://symfony.com/doc/7.4/deployment/proxies.html). To configure trusted proxies, use [Symfony semantic configuration](https://symfony.com/doc/7.4/deployment/proxies.html#solution-settrustedproxies) under the `framework.trusted_proxies` [configuration key](https://doc.ibexa.co/en/saas/administration/configuration/configuration/#configuration-files), for example: ```yaml framework: trusted_proxies: '192.0.0.1,10.0.0.0/8' ``` > **Caution: Careful when trusting dynamic IP that usesREMOTE_ADDRvalue or similar** > > On Upsun, Varnish doesn't have a static IP, like with [AWS LB](https://symfony.com/doc/7.4/deployment/proxies.html#but-what-if-the-ip-of-my-reverse-proxy-changes-constantly). For this reason, the `TRUSTED_PROXIES` env variable supports being set to value `REMOTE_ADDR`, which is equal to: > > ```php > use Symfony\Component\HttpFoundation\Request; > > /** @var \Symfony\Component\HttpFoundation\Request $request */ > Request::setTrustedProxies([$request->server->get('REMOTE_ADDR')], Request::HEADER_X_FORWARDED_FOR | Request::HEADER_X_FORWARDED_HOST | Request::HEADER_X_FORWARDED_PROTO | Request::HEADER_X_FORWARDED_PORT); > ``` > > When trusting remote IP like this, make sure your application is only accessible through Varnish. If it's accessible in other ways, this may result in trusting, for example, the IP of client browser instead, which would be a serious security issue. > > Make sure that **all** traffic always comes from the trusted proxy/load balancer, and that there is no other way to configure it. When using Fastly, you need to set `trusted_proxies` according to the [IP ranges used by Fastly](https://api.fastly.com/public-ip-list). > **Tip: Tip** > > You don't have to set `trusted_proxies` when using Fastly on Upsun. The Upsun router automatically changes the source IP of requests coming from Fastly, replacing the source IP with the actual client IP and removing any `X-FORWARD-...` header in the request before it reaches Cohesivo. For more information about setting these variables, see [Configuration examples](#configuration-examples). ### Update YML configuration Next, you need to tell Cohesivo to use an HTTP-based purge client (specifically the FosHttpCache Varnish purge client), and specify the URL that Varnish can be reached on: | Configuration | Parameter | Environment variable | Possible values | | ---------------------------------------------------------- | -------------------------- | ------------------------------------ | --------------------------------------------------------------------------------- | | `ibexa.http_cache.purge_type` | `purge_type` | `HTTPCACHE_PURGE_TYPE` | local, varnish, fastly | | `ibexa.system..http_cache.purge_servers` | `purge_server` | `HTTPCACHE_PURGE_SERVER` | Array of URLs to proxies when using Varnish or Fastly (`https://api.fastly.com`). | | `ibexa.system..http_cache.varnish_invalidate_token` | `varnish_invalidate_token` | `HTTPCACHE_VARNISH_INVALIDATE_TOKEN` | (Optional) For token-based authentication. | | `ibexa.system..http_cache.fastly.service_id` | `fastly_service_id` | `FASTLY_SERVICE_ID` | Service ID to authenticate with Fastly. | | `ibexa.system..http_cache.fastly.key` | `fastly_key` | `FASTLY_KEY` | Service key/token to authenticate with Fastly. | If you need to set multiple purge servers, configure them in the YAML configuration, instead of parameter or environment variable, as they only take single string value. Example configuration for Varnish as reverse proxy, providing that [front controller has been configured](#configure-symfony-front-controller): ```yaml ibexa: http_cache: purge_type: varnish system: # Assuming that my_siteaccess_group contains both your front-end and back-end SiteAccesses my_siteaccess_group: http_cache: # Fill in your Varnish server(s) address(es). purge_servers: [http://my.varnish.server:8081] ``` #### Varnish and Basic Auth If the Varnish server is protected by Basic Auth, specify the Basic Auth credentials within the `purge_servers` setting using the format: ```yaml ibexa: system: my_siteaccess_group: http_cache: purge_servers: [http://myuser:mypasswd@my.varnish.server:8081] ``` Varnish is enabled by default when using Ibexa Cloud and the `purge_servers` setting is set automatically. To enable Basic Auth on Ibexa Cloud when using Varnish, specify the credentials using the following environment variables to make sure that Varnish is reachable: ```bash env:HTTPCACHE_USERNAME=myuser env:HTTPCACHE_PASSWORD=mypasswd ``` If you want to use Basic Auth with Fastly on Ibexa Cloud, please see [Enable basic-auth on Fastly](https://doc.ibexa.co/en/saas/infrastructure_and_maintenance/cache/http_cache/fastly/#enable-basic-auth-on-fastly). > **Note: Invalidating Varnish cache by using tokens** > > In setups where the Varnish server IP can change (for example, on Ibexa Cloud), you can use token-based cache invalidation through [`ibexa_purge_acl`](https://github.com/ibexa/http-cache/blob/6.0/docs/varnish/vcl/varnish5.vcl#L174). > > In such situation, use strong, secure hash and make sure to keep the token secret. ### Ensure proper Captcha behavior (Experience) If your installation uses Varnish and you want users to be able to configure and use Captcha in their forms, you must enable sending Captcha data as a response to an Ajax request. Otherwise, Varnish doesn't allow for the transfer of Captcha data to the form, and as a result, users see an empty image. To enable sending Captcha over Ajax, add the following configuration: ```yaml ibexa: system: default: form_builder: captcha: use_ajax: true ``` ### Update custom Captcha block (Experience) If you created a custom Captcha block for your site by overriding the default file (`vendor/gregwar/captcha-bundle/Resources/views/captcha.html.twig`), you must make the following changes to the custom block template file: - change the name of the block to `ajax_captcha_widget` - include the JavaScript file: ```js {{ encore_entry_script_tags('ibexa-form-builder-ajax-captcha-js', null, 'ibexa') }} ``` - add a data attribute with a `fieldId` value: ```js data-field-id="{{ field.id }}" ``` As a result, your file should be similar to `vendor/ibexa/form-builder/src/bundle/Resources/views/themes/standard/fields/captcha.html.twig` file. For more information about configuring Captcha fields, see [Captcha field](https://doc.ibexa.co/en/saas/content_management/forms/work_with_forms/#captcha-field). ### Use Fastly as HttpCache proxy [Fastly](https://www.fastly.com/) delivers Varnish as a CDN service and is supported with Cohesivo. To learn how it works, see [Fastly documentation](https://www.fastly.com/documentation/guides/getting-started/concepts/using-fastlys-global-pop-network/). #### Configure Fastly in YML ```yaml ibexa: http_cache: purge_type: fastly system: # Assuming that my_siteaccess_group contains both your front-end and back-end SiteAccesses my_siteaccess_group: http_cache: purge_servers: [https://api.fastly.com] fastly: # See below for obtaining these values service_id: "ID" key: "token" ``` #### Configure Fastly using environment variables See the example below to configure Fastly with the `.env` file: ```bash HTTPCACHE_PURGE_TYPE="fastly" # Optional HTTPCACHE_PURGE_SERVER="https://api.fastly.com" # See below for obtaining service ID and application key/token FASTLY_SERVICE_ID="ID" FASTLY_KEY="token" ``` #### Configure Fastly on Ibexa Cloud If you use Upsun, it's recommended to configure all environment variables through [Upsun variables](https://fixed.docs.upsun.com/guides/ibexa/fastly.html). In Cohesivo, Varnish is enabled by default. To use Fastly, first you must [disable Varnish](https://fixed.docs.upsun.com/guides/ibexa/fastly.html#remove-varnish-configuration). #### Get Fastly service ID and API token To get the service ID, log in to . In the upper menu, click the **CONFIGURE** tab. The service ID is displayed next to the name of your service on any page. For instructions on how to generate a Fastly API token, see [the Fastly guide](https://www.fastly.com/documentation/guides/account-info/user-and-account-management/using-api-tokens/). The API token needs the `purge_all` an `purge_select` scopes. ### Configuration examples See below the most common configuration examples for the system, using environment variables. Example for Varnish with the `.env` file: ```bash HTTPCACHE_PURGE_TYPE="varnish" HTTPCACHE_PURGE_SERVER="http://varnish:80" ``` Example for Apache with `mod_env`: ```text SetEnv HTTPCACHE_PURGE_TYPE varnish SetEnv HTTPCACHE_PURGE_SERVER "http://varnish:80" ``` Example for Nginx: ```nginx fastcgi_param HTTPCACHE_PURGE_TYPE varnish; fastcgi_param HTTPCACHE_PURGE_SERVER "http://varnish:80"; ``` Example for Upsun: You can configure environment variables through [Upsun variables](https://fixed.docs.upsun.com/guides/ibexa/fastly.html). > **Tip: Tip** > > For HTTP cache, you most likely only use this for configuring Fastly for production and optionally staging, allowing `variables:env:` in `.platform.app.yaml` to, for example, specify Varnish or Symfony proxy as default for dev environment. #### Apache with Varnish ```text # mysite_com.conf # Configure Varnish SetEnv HTTPCACHE_PURGE_TYPE varnish SetEnv HTTPCACHE_PURGE_SERVER "http://varnish:80" # Configure IP of your Varnish server to be trusted proxy # !! Replace IP with the real one used by Varnish SetEnv TRUSTED_PROXIES "193.22.44.22" ``` #### Nginx with Fastly ```nginx # mysite_com.conf # Configure Fastly fastcgi_param HTTPCACHE_PURGE_TYPE fastly; fastcgi_param HTTPCACHE_PURGE_SERVER "https://api.fastly.com"; # See above for obtaining service ID and application key/token fastcgi_param FASTLY_SERVICE_ID "ID" fastcgi_param FASTLY_KEY "token" ``` ## Stale cache Stale cache, or grace mode in Varnish, occurs when: - Cache is served some time after the TTL expired. - When the back-end server doesn't respond. This has several benefits for high traffic installations to reduce load to the back end. Instead of creating several concurrent requests for the same page to the back end, the following happens when a page has been soft purged: - Next request hitting the cache triggers an asynchronous lookup to the back end. - If cache is still within grace period, first and subsequent requests for the content are served from cache, and don't wait for the asynchronous lookup to finish. - The back-end lookup finishes and refreshes the cache so any subsequent requests get a fresh cache. By default, Cohesivo always soft purges content on reverse proxies that support it (Varnish and Fastly), with the following logic in the out-of-the-box VCL: - Cache is within grace period. - Either the server isn't responding, or the request comes without a session cookie (anonymous user). Serving grace isn't always allowed by default because: - It's a safe default. Even if for anonymous users, stale cache can be confusing during acceptance testing. - It means REST API, which is used by the back office, would serve stale data, breaking the UI. > **Tip: Customizing stale cache handling** > > If you want to use grace handling for logged-in users as well, you can adapt the provided VCL to add a condition for opting out if the request has a cookie and the path contains REST API prefix to make sure the back office isn't negatively affected. > > If you want to disable grace mode, you can adapt the VCL to do hard instead of soft purges, or set grace/stale time to `0s`. # Context-aware HTTP cache > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). Context-aware HTTP cache caches requests depending on the logged-in user context. Cohesivo allows caching requests made by logged-in users. This is called (user) context-aware cache. It means that HTTP cache is unique per set of user permissions (roles and limitations), and there are variations of cache shared only among users that have the exact same permissions. So if a user browses a list of children locations, they only see children locations they have access to, even if their rendering is served from HTTP cache. This is accomplished by varying on a header called `X-User-Context-Hash`, which the system populates on the request. The [logic for this](#request-lifecycle) is accomplished in the provided VCL for Varnish and Fastly. A similar but internal logic is done in the provided enhanced Symfony Proxy (AppCache). ## Request lifecycle This expands steps covered in [FOSHttpCacheBundle documentation on user context feature](https://foshttpcachebundle.readthedocs.io/en/latest/features/user-context.html#how-it-works): 1. A client (browser) requests URI `/foo`. 2. The caching proxy receives the request and holds it. It first sends a hash request to the application's context hash route: `/_fos_user_context_hash`. 3. The application receives the hash request. An event subscriber (`UserContextSubscriber`) aborts the request immediately after the Symfony firewall is applied. The application calculates the hash (`HashGenerator`) and then sends a response with the hash in a custom header (`X-User-Context-Hash`). 4. The caching proxy receives the hash response, copies the hash header to the client's original request for `/foo` and restarts the modified original request. 5. If the response to `/foo` should differ per user context, the application sets a `Vary: X-User-Context-Hash` header, which makes Proxy store the variations of this cache varying on the hash value. The next time a request comes in from the same user, application lookup for the hash (step 3) doesn't take place, as the hash lookup itself is cached by the cache proxy as described below. ### User context hash caching Example of a response sent to reverse proxy from `/_fos_user_context_hash` with [Cohesivo's default config](#default-options-for-foshttpcachebundle): ```http HTTP/1.1 200 OK X-User-Context-Hash: Content-Type: application/vnd.fos.user-context-hash Cache-Control: public, max-age=600 Vary: Cookie, Authorization ``` In the example above the response is set to be cached for 10 minutes. It varies on the `Cookie` header to be able to cache it for the given user. To optimize it, the default VCL strips any cookie other than session cookies to make this work. It also varies on `Authorization` to cover any possible basic authorization headers in case that is used over sessions for some requests. > **Note: Problems with stale user hash** > > If you notice issues with stale hash usage, before you disable this cache, make sure login or logout always generates new session IDs and performs a full redirect to make sure no requests are being made with stale user context hashes. > **Caution: Limitations of the user context hash** > > If you use URI-based SiteAccess matching on a multi-repository installation (multiple databases), the default SiteAccess on the domain needs to point to the same repository (database), because `/_fos_user_context_hash` isn't SiteAccess-aware by default (see `ibexa.rest.default_router.non_siteaccess_aware_routes` parameter). This occurs because reverse proxy doesn't have knowledge about SiteAccesses and it doesn't pass the whole URL to be able to cache the user context hash response. > > The only known workaround is to make it SiteAccess aware, and have custom VCL logic tied to your SiteAccess matching with Varnish/Fastly, to send the SiteAccess prefix as URI. > **Caution: Default options for FOSHttpCacheBundle** > > The following configuration is defined by default for FOSHttpCacheBundle. You should not override these settings unless you know what you're doing. > > ```yaml > fos_http_cache: > proxy_client: > default: varnish > varnish: > http: > servers: ['$http_cache.purge_servers$'] > tag_mode: 'purgekeys' > > user_context: > enabled: true > hash_cache_ttl: 600 > # NOTE: These are also defined/used in AppCache, in Varnish VCL, and Fastly VCL > session_name_prefix: IBX_SESSION_ID > ``` ## Personalize responses Here are some generic recommendations on how to approach personalized content with Cohesivo / Symfony: 1. ESI with vary by cookie: Default VCL strips everything except session cookie, so this is effectively "per user". If you're on single-server setup without Varnish or Fastly, you can use the same cookie logic on the web server instead. This a low effort solution, and can be enough for one fragment that is reused across the whole site, for example, in header to show user name: Example: ```php use Symfony\Component\HttpFoundation\Response; // Inside a custom controller action, or even a Content View controller /** @var Response $response */ $response->setVary('Cookie'); ``` 2. Ajax/JS lookup to "uncached" custom Symfony controllers: This method doesn't consume memory in Varnish. It can optionally be cached with custom logic: Symfony Cache on server side and/or with client side caching techniques. This should be done as Ajax/JS lookup to avoid the uncached request that slows down the whole delivery of Vanish if it's done as ESI. This solution requires more effort depending on project requirements (for example, traffic load). 3. Custom vary by logic, for example, `X-User-Preference-Hash` inspired by `X-User-Context-Hash`: This method allows for fine-grained caching as you can explicitly vary on this in only the places that need it. This solution requires more effort (controller, VCL logic and adapting your own code), see the examples below. > **Tip: Dealing with paywall use cases** > > If you need to handle a paywall on a per-item basis, or example, do a lookup to backend for each URL where this is relevant. > > You can find an example for paywall authorization in [FOSHTTPCache documentation](https://foshttpcache.readthedocs.io/en/latest/user-context.html#alternative-for-paywalls-authorization-request). ### Best practices for custom vary by logic For information on how user context hashes are generated, see [FOSHttpCacheBundle documentation](https://foshttpcachebundle.readthedocs.io/en/latest/features/user-context.html#generating-hashes). Cohesivo implements a custom context provider to make user context hash reflect the current user's roles and limitations. This is needed given Cohesivo's more complex permission model compared to Symfony's. You can technically extend the user context hash by [implementing your own custom context provider(s)](https://foshttpcachebundle.readthedocs.io/en/latest/reference/configuration/user-context.html#custom-context-providers). However, **this is strongly discouraged** as it means increasing the amount of cache variations stored in proxy for every single cache item, lowering cache hit ratio and increasing memory use. Instead, you can create your own hash header for use cases where you need it. This way only controllers and views that really vary by your custom logic varies on it. You can use several methods to do it, ranging from completely custom VCL logic and dedicated controller to respond with hash to trusted proxy lookups, but this means additional lookups. ### Example for custom vary by logic You can extend `/_fos_user_context_hash` lookup to add another HTTP header with custom hash for your needs, and adapt the user context hash VCL logic to use the additional header. To avoid overloading any application code, take advantage of Symfony's event system: 1. Add a [Response event (`kernel.response`)](https://symfony.com/doc/7.4/reference/events.html#kernel-response) [listener or subscriber](https://symfony.com/doc/7.4/event_dispatcher.html) to add your own hash to `/_fos_user_context_hash`: ```php use Symfony\Component\EventDispatcher\EventSubscriberInterface; use Symfony\Component\HttpKernel\Event\ResponseEvent; final class MyEventSubscriber implements EventSubscriberInterface { public static function getSubscribedEvents(): array { return [ ResponseEvent::class => 'addPreferenceHash', ]; } public function addPreferenceHash(ResponseEvent $event): void { $response = $event->getResponse(); if ($response->headers->get('Content-Type') !== 'application/vnd.fos.user-context-hash') { return; } $response->headers->set('X-User-Preference-Hash', ''); } } ``` 2. Adapt VCL logic to pass the header to requests: ```diff @@ -174,6 +174,7 @@ sub ibexa_user_context_hash { if (req.restarts == 0 && (req.http.accept ~ "application/vnd.fos.user-context-hash" || req.http.x-user-context-hash + || req.http.x-user-preference-hash ) ) { return (synth(400, "Bad Request")); @@ -263,12 +264,19 @@ sub vcl_deliver { && resp.http.content-type ~ "application/vnd.fos.user-context-hash" ) { set req.http.x-user-context-hash = resp.http.x-user-context-hash; + set req.http.x-user-preference-hash = resp.http.x-user-preference-hash; return (restart); } // If we get here, this is a real response that gets sent to the client. + // Remove the vary on user preference hash, no need to expose this publicly. + if (resp.http.Vary ~ "X-User-Preference-Hash") { + set resp.http.Vary = regsub(resp.http.Vary, "(?i),? *X-User-Preference-Hash *", ""); + set resp.http.Vary = regsub(resp.http.Vary, "^, *", ""); + } + // Remove the vary on user context hash, this is nothing public. Keep all // other vary headers. if (resp.http.Vary ~ "X-User-Context-Hash") { ``` 3. Add `Vary` in your custom controller or content view controller: ```php use Symfony\Component\HttpFoundation\Response; /** @var Response $response */ $response->setVary('X-User-Preference-Hash'); // If you _also_ need to vary on Cohesivo permissions, instead use: //$response->setVary(['X-User-Context-Hash', 'X-User-Preference-Hash']); ``` # Content-aware HTTP cache > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). Content-aware HTTP cache takes into account the content it's connected to. HTTP cache in Cohesivo is aware of which content or entity it's connected to. This awareness is accomplished by means of cache tagging. All supported reverse proxies are content-aware. > **Note: Tag header is stripped in production for security reasons** > > For security reasons this header, and other internal cache headers, are stripped from output in production by the reverse proxy (in VCL for Varnish and Fastly). ## Cache tags Understanding tags is the key to making the most of Cohesivo's HTTP cache. Tags form a secondary set of keys assigned to every cache item, on top of the "primary key" which is the URI. Like an index in a database, a tag is typically used for anything relevant that represents the given cache item. Tags are used for cache invalidation. For example, the system tags every article response, and when the article content type is updated, it tells Varnish that all articles should be considered stale and updated in the background when someone requests them. Current content tags (and when the system purges on them): - Content: `c` - Purged on all smaller or larger changes to content (including its metadata, fields and locations). - Content version: `cv` - Purged when any version of Content is changed (for example, a draft is created or removed). - Content type: `ct` - Used when the content type changes, affecting content of its type. - Location: `l` - Used for clearing all cache relevant for a given location. - Parent Location: `pl<[parent-]location-id>` - Used for clearing all children of a location (`pl`), or all siblings (`pl`). - Path: `p` - For operations that change the tree itself, for example, move or remove. - Relation: `r` - Only purged on when content updates are severe enough to also affect reverse relations. - Relation location: `rl` - Same as relation, but by location ID. > **Note: Automatic repository prefixing of cache tags** > > As Cohesivo supports multi-repository (multi-database) setups that can have overlapping IDs, the shared HTTP cache systems need to distinguish tags relevant to the different content repositories. > > This is why in multi-repository setup you can see cache tags such as `1p2`. In this example `1` represents the index among configured repositories, meaning the second repository in the system. > > Tags aren't prefixed for default repository (index "0"). The content tags are returned in a header in the responses from Cohesivo. The header name is dependent on which HTTP Cache Cohesivo is configured with: - Symfony reverse proxy: `X-Cache-Tags` - Varnish: `xkey` - Fastly: `Surrogate-Key` Examples: - `X-Cache-Tags: ez-all,c52,ct42,l2,pl1,p1,p2,r56,r57` - `xkey: ez-all c52 ct42 l2 pl1 p1 p2 r56 r57` - `Surrogate-Key: ez-all c52 ct42 l2 pl1 p1 p2 r56 r57` ### Troubleshooting - Cache header too long errors In case of complex content, for example, Pages with many blocks, or RichText with a lot of embeds/links, you can encounter problems with too long cache header on responses. It happens because necessary cache entries may not be tagged properly. You may also see `502 Headers too long` errors, and webserver refusing to serve the page. You can solve this issue in one of the following ways: #### A. Allow larger headers Varnish configuration: - [`http_resp_hdr_len`](https://www.varnish.org/docs/reference/varnishd/#http_resp_hdr_len) (default 8k, change to for example, 32k) - [`http_max_hdr`](https://www.varnish.org/docs/reference/varnishd/#http_max_hdr) (default 64, change to for example, 128) - [`http_resp_size`](https://www.varnish.org/docs/reference/varnishd/#http_resp_size) (default 23k, change to for example, 96k) - [`workspace_backend`](https://www.varnish.org/docs/reference/varnishd/#workspace_backend) (default 64k, change to for example, 128k) If you need to see these long headers in `varnishlog`, adapt the [`vsl_reclen`](https://www.varnish.org/docs/reference/varnishd/#vsl_reclen) setting. Nginx has a default limit of 4k/8k when buffering responses: - For [PHP-FPM](https://www.php.net/manual/en/install.fpm.php) setup using proxy module, configure [`proxy_buffer_size`](https://nginx.org/en/docs/http/ngx_http_proxy_module.html#proxy_buffer_size) - For FastCGI setup using fastcgi module, configure [`fastcgi_buffer_size`](https://nginx.org/en/docs/http/ngx_http_fastcgi_module.html#fastcgi_buffer_size) Fastly has a `Surrogate-Key` header limit of 16 kB, and this cannot be changed. Apache has a [hard](https://github.com/apache/httpd/blob/5f32ea94af5f1e7ea68d6fca58f0ac2478cc18c5/server/util_script.c#L495) [coded](https://github.com/apache/httpd/blob/7e2d26eac309b2d79e467ef586526c10e0f226f8/include/httpd.h#L299-L303) limit of 8 kB, so if you face this issue consider using Nginx instead. #### B. Limit tags header output by system 1. For inline rendering displaying the content name, image attribute, and/or link, it would be enough to: - Look into how many inline (non ESI) render calls for content rendering you're doing, and see if you can organize it differently. - Consider inlining the views not used elsewhere in the given template and [tagging the response in Twig](#response-tagging-in-templates) with "relation" tags. - (Optional) You can set reduced cache TTL for the given view, to reduce the risk of stale cache on subtree operations affecting the inlined content. 2. You can opt in to set a max length parameter (in bytes) and corresponding ttl (in seconds) for cases when the limit is reached. The system logs a warning where the limit is reached, and when needed, you can optimize these cases as described above. ```yaml parameters: # Warning, setting this means you risk losing tag information, risking stale cache. Here set below 8k: ibexa.http_cache.tags.header_max_length: 7900 # In order to reduce risk of stale cache issues, you should set a lower TTL here then globally (here set as 2h) ibexa.http_cache.tags.header_reduced_ttl: 7200 ``` ## Response tagging with content view For content views response tagging is done automatically, and cache system outputs headers as follows: ```http HTTP/1.1 200 OK Cache-Control: public, max-age=86400 xkey: ez-all c1 ct1 l2 pl1 p1 p2 ``` If the given content has several locations, you can see several `l` and `p` tags in the response. > **Note: How response tagging for ContentView is done internally** > > In `ibexa/http-cache` there is a dedicated response listener `HttpCacheResponseSubscriber` that checks if: > > - the response has attribute `view` > - the view implements `Ibexa\Core\MVC\Symfony\View\CachableView` > - cache isn't disabled on the individual view > > If that checks out, the response is adapted with the following: > > - `ResponseCacheConfigurator` applies SiteAccess settings for enabled/disabled cache and default TTL. > - `DispatcherTagger` dispatches the built-in ResponseTaggers which generate the tags as described above. ### ResponseConfigurator A `ReponseCacheConfigurator` configures an HTTP Response object, makes the response public, adds tags, and sets the shared max age. It's provided to `ReponseTaggers` that use it to add the tags to the response. The `ConfigurableResponseCacheConfigurator` (`Ibexa\HttpCache\ResponseConfigurator\ConfigurableResponseCacheConfigurator`) follows the `view_cache` configuration and only enables cache if it's enabled in the configuration. ### Delegator and Value taggers - Delegator taggers - extract another value or several from the given value and pass it on to another tagger. For example, a `ContentView` is covered both by the `ContentValueViewTagger` and `LocationValueViewTagger`, where the first extracts the content from the `ContentView` and passes it to the `ContentInfoTagger`. - Value taggers - extract the `Location` and pass it on to the `LocationViewTagger`. The built-in taggers support the following value types: - [`ContentInfo`](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-Core-Repository-Values-Content-ContentInfo.html) - [`Location`](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-Core-Repository-Values-Content-Location.html) - Any view implementing `Ibexa\Core\MVC\Symfony\View\ContentValueView` - Any view implementing `Ibexa\Core\MVC\Symfony\View\LocationValueView` ## DispatcherTagger Accepts any value and passes it on to taggers registered with the service tag `ibexa.cache.http.response.tagger` supporting given type. If you pass a value which no tagger supports (for example, a [`Content`](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-Core-Repository-Values-Content-Content.html) object), the system logs a warning. When [`kernel.debug`](https://symfony.com/doc/7.4/reference/configuration/kernel.html#kernel-debug) is enabled, an exception is thrown to help you catch unsupported types early. ## Response tagging in controllers For tagging needs in controllers, there are several options, here presented in recommended order: 1. Reusing `DispatcherTagger` to pick correct tags. Examples for tagging everything needed for content using the autowireable [`ResponseTagger`](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-HttpCache-ResponseTagger-ResponseTagger.html) interface: ```php /** @var \Ibexa\Contracts\HttpCache\ResponseTagger\ResponseTagger $responseTagger */ /** @var \Ibexa\Core\MVC\Symfony\View\ContentValueView|\Ibexa\Core\MVC\Symfony\View\LocationValueView $view */ $responseTagger->tag($view); // When working with a view /** @var \Ibexa\Contracts\Core\Repository\Values\Content\Content $content */ $responseTagger->tag($content->getContentInfo()); // When working with a content item /** @var \Ibexa\Contracts\Core\Repository\Values\Content\Location $location */ $responseTagger->tag($location); // When working with a location ``` 2. Use `ContentTagInterface` API for content related tags. Examples for adding specific content tags using the autowireable `ContentTagInterface`: ```php /** * @var \Ibexa\Contracts\HttpCache\Handler\ContentTagInterface $tagHandler * @var \Ibexa\Contracts\Core\Repository\Values\Content\Content $content * @var \Ibexa\Contracts\Core\Repository\Values\Content\Location $location */ // Example for tagging everything needed for Content: $tagHandler->addContentTags([$content->id]); $tagHandler->addLocationTags([$location->id]); $tagHandler->addParentLocationTags([$location->parentLocationId]); $tagHandler->addPathTags($location->path); $tagHandler->addContentTypeTags([$content->getContentType()->id]); // Example when using ESI as also shown below using FOS tag handler (there is also a method for relation locations): $tagHandler->addRelationTags([33, 44]); ``` 3. Manually add tags yourself using low-level FOS `TagHandler`. In PHP, FOSHttpCache exposes the `fos_http_cache.http.symfony_response_tagger` service which enables you to add tags to a response. The following example adds minimal tags when ID 33 and 34 are rendered in ESI, but parent response needs these tags to get refreshed if they're deleted: ```php use Ibexa\Contracts\HttpCache\Handler\ContentTagInterface; /** @var \FOS\HttpCacheBundle\Http\SymfonyResponseTagger $responseTagger */ $responseTagger->addTags([ContentTagInterface::RELATION_PREFIX . '33', ContentTagInterface::RELATION_PREFIX . '34']); ``` See [Tagging from code](https://foshttpcachebundle.readthedocs.io/en/latest/features/tagging.html#tagging-from-twig-templates) in FOSHttpCacheBundle doc. 4. Use deprecated `X-Location-Id` header. For custom or built-in controllers (for example, REST) that still use `X-Location-Id`, `XLocationIdResponseSubscriber` handles translating this header to tags. It supports singular and comma-separated location ID value(s): ```php /** @var \Symfony\Component\HttpFoundation\Response $response */ $response->headers->set('X-Location-Id', '123'); // Alternatively using several Location ID values $response->headers->set('X-Location-Id', '123,212,42'); ``` > **Caution: X-Location-Id use is deprecated** > > `X-Location-Id` is deprecated and removed in future. For rendering content it's advised to refactor to use content view, if not applicable `ContentTagInterface` or lastly manually output tags. ## Response tagging in templates 1. `ibexa_http_cache_tag_location()` For full content tagging when inline rendering, use the following: ```html+twig {{ ibexa_http_cache_tag_location(location) }} ``` 2. `ibexa_http_cache_tag_relation_ids()` or `ibexa_http_cache_tag_relation_location_ids()` When you want to reduce the amount of tags, or the inline content is rendered using ESI, a minimum set of tags can be set: ```html+twig {{ ibexa_http_cache_tag_relation_ids(content.id) }} {# Or using array for several values #} {{ ibexa_http_cache_tag_relation_location_ids([field1.value.destinationContentId, field2.value.destinationContentId]) }} ``` 3. `{{ fos_httpcache_tag(['r33', 'r44']) }}` As a last resort you can also use the following function from FOS which lets you set low level tags directly: ```html+twig {{ fos_httpcache_tag('r33') }} {# Or using array for several values #} {{ fos_httpcache_tag(['r33', 'r44']) }} ``` See [Tagging from Twig Templates](https://foshttpcachebundle.readthedocs.io/en/latest/features/tagging.html#tagging-from-twig-templates) in FOSHttpCacheBundle documentation. ## Tag purging ### Default tag purging `ibexa/http-cache` uses repository API event subscribers to listen to events emitted on repository operations, and depending on the operation triggers expiry on a specific tag or set of tags. All event subscribers can be found in `http-cache/src/lib/EventSubscriber/CachePurge`. ### Tags purged on publish event Below is an example of a content structure. The tags which the content view controller adds to each location are also listed: ```text - [Home] (content-id=52, location-id=2) ez-all c52 ct42 l2 pl1 p1 p2 | - [Parent1](content-id=53, location-id=20) ez-all c53 ct1 l20 pl2 p1 p2 p20 | - [Child](content-id=55, location-id=22) ez-all c55 ct1 l22 pl20 p1 p2 p20 p22 - [Parent2](content-id=54, location-id=21) ez-all c55 ct1 l22 pl2 p1 p2 p22 ``` In the event when a new version of `Child` is published, the following keys are purged: - `c55`, because Content `[Child]` was changed - `r55`, because cache for any object that has a relation to Content `[Child]` should be purged - `l22`, because location `[Child]` has changed ( that would be location holding content-id=55) - `pl22`, because cache for children of `[Child]` should be purged - `rl22`, because cache for any object that has a relation to Location `[Child]` should be purged - `l20`, because cache for parent of `[Child]` should be purged - `pl20`, because cache for siblings of `[Child]` should be purged In summary, HTTP Cache for any location representing `[Child]`, any Content that relates to the Content `[Child]`, the location for `[Child]`, any children of `[Child]`, any location that relates to the location `[Child]`, location for `[Parent1]`, any children on `[Parent1]`. Effectively, in this example HTTP cache for `[Parent1]` and `[Child]` is cleared. ### Tags purged on move event With the same content structure as above, the `[Child]` location is moved below `[Parent2]`. The new structure is then: ```text - [Home] (content-id=52, location-id=2) ez-all c52 ct42 l2 pl1 p1 p2 | - [Parent1](content-id=53, location-id=20) ez-all c53 ct1 l20 pl2 p1 p2 p20 - [Parent2](content-id=54, location-id=21) ez-all c55 ct1 l22 pl2 p1 p2 p22 | - [Child](content-id=55, location-id=22) ez-all c55 ct1 l22 pl21 p1 p2 p21 p22 ``` The following keys are purged during the move: - `l20`, because cache for previous parent of `[Child]` should be purged (`[Parent1]`) - `pl20`, because cache for children of `[Parent1]` should be purged - `l21`, because cache for new parent of `[Child]` should be purged (`[Parent2]`) - `pl21`, because cache for all children of new parent (`[Parent2]`) should be purged - `p22`, because cache for any element below `[Child]` should be purged (because path has changed) In other words, HTTP Cache for `[Parent1]`, children of `[Parent1]` ( if any ), `[Parent2]`, children of `[Parent2]` ( if any ), `[Child]` and any subtree below `[Child]`. ### Custom purging from code While the system purges tags whenever API is used to change data, you may need to purge directly from code. For that you can inject the built-in [`PurgeClientInterface`](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-HttpCache-PurgeClient-PurgeClientInterface.html) by using the `ibexa.http_cache.purge_client` service name: ```php use Ibexa\Contracts\HttpCache\Handler\ContentTagInterface; use Ibexa\Contracts\HttpCache\PurgeClient\PurgeClientInterface; use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Style\SymfonyStyle; use Symfony\Component\DependencyInjection\Attribute\Autowire; #[AsCommand(name: 'app:purge-cache')] class MyCustomCacheCommand { public function __construct( #[Autowire(service: 'ibexa.http_cache.purge_client')] private readonly PurgeClientInterface $purgeClient ) { } public function __invoke(SymfonyStyle $io): int { // Example for purging by Location ID: $locationId = 2; $this->purgeClient->purge([ContentTagInterface::LOCATION_PREFIX . $locationId]); // Example for purging all cache for instance for full re-deploy cases // Usually this triggers an expiry (soft purge): $this->purgeClient->purgeAll(); return Command::SUCCESS; } } ``` ### Purging from command line Example for purging by location and by content ID: ```bash bin/console fos:httpcache:invalidate:tag l44 c33 ``` Example for purging by all cache: ```bash bin/console fos:httpcache:invalidate:tag ez-all ``` > **Tip: Purge is done on the current repository** > > Similarly to purging from code, the tags you purge on, are prefixed to match the currently configured SiteAccess. When you use this command in combination with multi-repository setup, make sure to specify SiteAccess argument. ## Testing and debugging HTTP cache It's important to test your code in an environment which is as similar as your production environment as possible. That means that if only are testing locally using the default Symfony Reverse proxy when your are going to use Varnish or Fastly in production, you're likely ending up some (bad) surprises. Due to the Symfony reverse proxy's lack of support for ESIs, it behaves quite different from Varnish and Fastly in some aspects. If you're going to use Varnish in production, make sure you also test your code with Varnish. If you're going to use Fastly in production, testing with Fastly in your developer install is likely not feasible (you're local development environment must then be accessible for Fastly). Testing with Varnish instead in most cases does the job. But if you need to change the varnish configuration to make your site work, be aware that Varnish and Fastly uses different dialects, and that .vcl code for Varnish V6.x doesn't likely work as-is on Fastly. This section describes to how to debug problems related to HTTP cache. ```text You must be able to look both at responses and headers Cohesivo sends to HTTP cache, and not so much at responses and headers the HTTP cache sends to the client (web browser). It means you must be able to send requests to your origin (web server) that don't go through Varnish or Fastly. If you run Nginx and Varnish on premise, you should know what host and port number both Varnish and Nginx runs on. ``` If you perform tests on Fastly enabled environment on Ibexa Cloud provided by Upsun, you need to use the Upsun dashboard to obtain the endpoint for Nginx. The following example shows how to debug and check why Fastly doesn't cache the front page properly. If you run the command multiple times: `curl -IXGET https://www.staging.foobar.com.us-2.platformsh.site` it always outputs: ```http HTTP/2 200 (...) x-cache: MISS ``` ### Nginx endpoint on Ibexa Cloud #### Finding Nginx endpoint for environments located on the grid To find the Nginx point, first, you need to know in which region your project is located. To do that, go to the Ibexa Cloud dashboard. To find a valid route, click an element in the **URLs** drop-down for the specified environment and select the route. A route may look like this: `https://www.staging.foobar.com.us-2.platformsh.site/` In this case the region is `us-2` and you can find the public IP list on [Upsun documentation page](https://fixed.docs.upsun.com/development/regions.html#public-ip-addresses). Typically, you can add a `gw` to the hostname and use nslookup to find it. ```bash $ nslookup > gw.us-2.platformsh.site (...) Address: 1.2.3.4 ``` You can also use the [Ibexa Cloud CLI](https://cli.ibexa.cloud/) (which has the same command as the Upsun CLI) to find [the endpoint](https://fixed.docs.upsun.com/domains/steps/dns.html): ```bash ibexa_cloud environment:info edge_hostname ``` #### Finding Nginx endpoint on dedicated cloud If you have a dedicated 3-node cluster on Upsun, the procedure for getting the endpoint to environments that are located on that cluster (`production` and sometimes also `staging`) is slightly different. In the **URLs** drop-down in the Ibexa Cloud dashboard, find the route that has the format `somecontent.[clusterid].ent.platform.sh/`, for example, `myenvironment.abcdfg2323.ent.platform.sh/` The endpoint in case has the format `c.[clusterid].ent.platform.sh`, for example, `c.asddfs2323.ent.platform.sh/`. Next, use nslookup to find the IP: ```bash $ nslookup > c.asddfs2323.ent.platform.sh (...) Address: 1.2.3.4 ``` ### Fetching user context hash As explained in [User Context Hash caching](https://doc.ibexa.co/en/saas/infrastructure_and_maintenance/cache/http_cache/context_aware_cache/#user-context-hash-caching), the HTTP cache indexes the cache based on the user-context-hash. Users with the same user-context-hash share the same cache (as long as Cohesivo responds with `Vary: X-User-Context-Hash`). To simulate the requests the HTTP cache sends to Cohesivo, you need this user-context-hash. To obtain it, use `curl`. ```bash curl -IXGET --resolve www.staging.foobar.com.us-2.platformsh.site:443:1.2.3.4 --header "Surrogate-Capability: abc=ESI/1.0" --header "accept: application/vnd.fos.user-context-hash" --header "x-fos-original-url: /" https://www.staging.foobar.com.us-2.platformsh.site/_fos_user_context_hash ``` Some notes about each of these parameters: - `-IXGET`, one of many ways to tell curl that we want to send a GET request, but we are only interested in outputting the headers - `--resolve www.staging.foobar.com.us-2.platformsh.site:443:1.2.3.4` - We tell curl not to do a DNS lookup for `www.staging.foobar.com.us-2.platformsh.site`. We do that because in our case that resolves to the Fastly endpoint, not our origin (nginx) - We specify `443` because we are using `https` - We provide the IP of the nginx endpoint at Upsun (`1.2.3.4` in this example) - `--header "Surrogate-Capability: abc=ESI/1.0"`, strictly speaking not needed when fetching the user-context-hash, but this tells Cohesivo that client understands ESI tags. It's good practice to always include this header when imitating the HTTP Cache. - `--header "accept: application/vnd.fos.user-context-hash"` tells Cohesivo that the client wants to receive the user-context-hash - `--header "x-fos-original-url: /"` is required by the fos-http-cache bundle to deliver the user-context-hash - `https://www.staging.foobar.com.us-2.platformsh.site/_fos_user_context_hash` : here we use the hostname we earlier told curl how to resolve using `---resolve`. `/_fos_user_context_hash` is the route to the controller that are able to deliver the user-context-hash. - You may also provide the session cookie (\`--cookie ".....=....") for a logged-in-user if you're interested in the x-user-context-hash for a different user but anonymous The output for this command should look similar to this: ```http HTTP/1.1 200 OK Server: nginx/1.27.0 Content-Type: application/vnd.fos.user-context-hash Transfer-Encoding: chunked Connection: keep-alive X-User-Context-Hash: daea248406c0043e62997b37292bf93a8c91434e8661484983408897acd93814 Cache-Control: max-age=600, public Date: Tue, 31 Aug 2021 13:35:00 GMT Vary: Origin Vary: cookie Vary: authorization X-Cache-Debug: 1 Surrogate-Key: ez-user-context-hash ez-all fos_http_cache_hashlookup- ``` The header `X-User-Context-Hash` is the one of the interest here, but you may also note the `Surrogate-Key` which holds the [cache tags](#cache-tags). ### Fetching HTML response Now you have the user-context-hash, and you can ask origin for the actual resource you're after: ```bash curl -IXGET --resolve www.staging.foobar.com.us-2.platformsh.site:443:1.2.3.4 --header "Surrogate-Capability: abc=ESI/1.0" --header "x-user-context-hash: daea248406c0043e62997b37292bf93a8c91434e8661484983408897acd93814" https://www.staging.foobar.com.us-2.platformsh.site/ ``` The output : ```http HTTP/1.1 200 OK Server: nginx/1.27.0 Content-Type: text/html; charset=UTF-8 Transfer-Encoding: chunked Connection: keep-alive Cache-Control: public, s-maxage=86400 Date: Wed, 01 Sep 2021 07:18:27 GMT X-Cache-Debug: 1 Vary: X-User-Context-Hash Vary: X-Editorial-Mode Surrogate-Control: content="ESI/1.0" Surrogate-Key: ez-all c52 ct42 l2 pl1 p1 p2 r56 r57 ``` The `Cache-Control` header tells the HTTP cache to store the result in the cache for 1 day (86400 seconds) The `Vary: X-User-Content-Hash` header tells the HTTP cache that this cache element may be used for all users which has the given `x-user-hash` (`daea248406c0043e62997b37292bf93a8c91434e8661484983408897acd93814`). The document might also be removed from the cache by purging any of the keys provided in the `Surrogate-Key` header. So back to the original problem here. This resource is for some reason not cached by Fastly (remember the `x-cache: MISS` we started with). But origin says this page can be cached for 1 day. How can that be? The likely reason is that this page also contains some ESI fragments and that one or more of these aren't cacheable. So, first let's see if there are any ESIs here. We remove the `-IXGET` options (to see content of the response, not only headers) to curl and search for esi: ```bash curl --resolve www.staging.foobar.com.us-2.platformsh.site:443:1.2.3.4 --header "Surrogate-Capability: abc=ESI/1.0" --header "x-user-context-hash: daea248406c0043e62997b37292bf93a8c91434e8661484983408897acd93814" https://www.staging.foobar.com.us-2.platformsh.site/ | grep esi ``` The output is: ```HTML ``` Now, investigate the response of each of these ESI fragments to understand what is going on. It's important to put that URL in single quotes as the URLS to the ESIs include special characters that can be interpreted by the shell. #### 1st ESI ```bash curl -IXGET --resolve www.staging.foobar.com.us-2.platformsh.site:443:1.2.3.4 --header "Surrogate-Capability: abc=ESI/1.0" --header "x-user-context-hash: daea248406c0043e62997b37292bf93a8c91434e8661484983408897acd93814" 'https://www.staging.foobar.com.us-2.platformsh.site/_fragment?_hash=B%2BLUWB2kxTCc6nc5aEEn0eEqBSFar%2Br6jNm8fvSKdWU%3D&_path=locationId%3D2%26contentId%3D52%26blockId%3D11%26versionNo%3D3%26languageCode%3Deng-GB%26serialized_siteaccess%3D%257B%2522name%2522%253A%2522site%2522%252C%2522matchingType%2522%253A%2522default%2522%252C%2522matcher%2522%253Anull%252C%2522provider%2522%253Anull%257D%26serialized_siteaccess_matcher%3Dnull%26_format%3Dhtml%26_locale%3Den_GB%26_controller%3DEzSystems%255CEzPlatformPageFieldTypeBundle%255CController%255CBlockController%253A%253ArenderAction' ``` This ESI is handled by a controller in the `FieldTypePage` bundle provided by Cohesivo. The output is: ```http HTTP/1.1 200 OK Server: nginx/1.27.0 Content-Type: text/html; charset=UTF-8 Transfer-Encoding: chunked Connection: keep-alive Cache-Control: public, s-maxage=86400 Date: Wed, 01 Sep 2021 07:51:40 GMT Vary: Origin Vary: X-User-Context-Hash Vary: X-Editorial-Mode X-Cache-Debug: 1 Surrogate-Key: ez-all c52 l2 ``` The headers here look correct and don't indicate that this ESI isn't cached by the HTTP cache. The second ESI has a similar response. #### 3rd ESI ```bash curl -IXGET --resolve www.staging.foobar.com.us-2.platformsh.site:443:1.2.3.4 --header "Surrogate-Capability: abc=ESI/1.0" --header "x-user-context-hash: daea248406c0043e62997b37292bf93a8c91434e8661484983408897acd93814" 'https://www.staging.foobar.com.us-2.platformsh.site//_fragment?_hash=lnKTnmv6bb1XpaMPWRjV3sNazbn9rDXskhjGae1BDw8%3D&_path=locationId%3D2%26contentId%3D52%26blockId%3D13%26versionNo%3D3%26languageCode%3Deng-GB%26serialized_siteaccess%3D%257B%2522name%2522%253A%2522site%2522%252C%2522matchingType%2522%253A%2522default%2522%252C%2522matcher%2522%253Anull%252C%2522provider%2522%253Anull%257D%26serialized_siteaccess_matcher%3Dnull%26_format%3Dhtml%26_locale%3Den_GB%26_controller%3DEzSystems%255CCustomBundle%255CController%255CFooController%253A%253AcustomAction' ``` This ESI is handled by a custom `FooController::customAction` and the output of the command is: Output: ```http HTTP/1.1 200 OK Server: nginx/1.27.0 Content-Type: text/html; charset=UTF-8 Transfer-Encoding: chunked Connection: keep-alive Set-Cookie: IBX_SESSION_ID21232f297a57a5a743894a0e4a801fc3=asrpqgmh5ll5ssseca3cov8er7; path=/; HttpOnly; SameSite=lax Cache-Control: public, s-maxage=86400 Date: Wed, 01 Sep 2021 07:51:40 GMT Vary: Origin Vary: X-User-Context-Hash Vary: X-Editorial-Mode X-Cache-Debug: 1 Surrogate-Key: ez-all ``` The `Cache-Control` and `Vary` headers look correct. The request is handled by a custom controller and the `Surrogate-Key` only contains the default `ez-all` value. This isn't a problem as long as the controller doesn't return values from any content in the Cohesivo repository. If it does, the controller should also add the corresponding IDs to such objects in that header. The `Set-Cookie` here may cause the problem. A ESI fragment should never set a cookie because: - Clients only receive the headers set in the "mother" document (the headers in the "/" response in this case). - Only the content of ESIs responses is returned to the client. **No headers set in the ESI response ever reach the client**. ESI headers are only seen by the HTTP cache. - Symfony reverse proxy doesn't support ESIs at all, and any ESI calls (`render_esi()`) are implicitly replaced by sub-requests (`render()`). So any `Set-Cookie` **is** sent to the client when using Symfony reverse proxy. - Fastly flags it resource as "not cacheable" because it set a cookie at least once. Even though that endpoint stops setting cookies, Fastly still doesn't cache that fragment. Any document referring to that ESI is a `MISS`. Fastly cache needs to be purged (`Purge-all` request) to remove this flag. - It means that it's not recommended to always initiate a session when loading the front page. You must ensure that you don't unintendedly start a session in a controller used by ESIs, for example, when trying to access as session variable before a session has been initiated yet. # Configure and customize Fastly > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). Configure Fastly for use with Cohesivo. You can configure Fastly by using API calls or through the Fastly Web Interface. Fastly provides a [Fastly CLI](https://www.fastly.com/documentation/reference/cli/) for configuring Fastly through its API. Ibexa Cloud is delivered with Fastly preconfigured. It means that you don't have to do any changes to the Fastly configuration to make your site work. The information provided here is only applicable if you want to change the default Fastly configuration on Ibexa Cloud, or if you're not using Ibexa Cloud and want to configure Fastly to work with Cohesivo on premise. > **Note: The Fastly Web Interface isn't available for Ibexa Cloud** > > It's recommend for Ibexa Cloud customers to use the Fastly CLI instead of using the Fastly API directly with `curl`, or other alternatives. > **Note: Disable Varnish when you use Fastly** > > Varnish is automatically provisioned on Ibexa Cloud. Varnish needs to be disabled on all environments that use Fastly. See [documentation on how to do that](https://fixed.docs.upsun.com/guides/ibexa/fastly.html). ## Prepare for using Fastly locally These steps aren't needed when you use Ibexa Cloud, because Fastly is preconfigured in it. ### Get Fastly credentials from Ibexa Cloud installation To use Fastly CLI or Fastly API directly, you need to obtain the credentials for your site. To obtain the credentials, connect to your Fastly-enabled environment (for example, production or staging) through SSH and run the following command: ```bash declare|grep FASTLY FASTLY_KEY=... FASTLY_SERVICE_ID=... ``` These credentials are different for your production and staging environments. When you configure the Fastly CLI, use the credentials for the environment that you want to change. > **Note: Different environment variable names between products** > > When you configure Fastly CLI, you use the `FASTLY_API_TOKEN` variable to store the token, while with Cohesivo you use `FASTLY_KEY` for the same purpose. ### Quickly configure Fastly for use with Cohesivo Use the commands below to install VCL configuration required for running Fastly with Cohesivo. You also need to set up domains, HTTPS and origin configuration (not covered here). All commands are explained in detail [below](#view-and-modify-vcl-configuration): ```bash fastly vcl custom create --name=ibexa_main.vcl --version=active --autoclone --content=vendor/ibexa/fastly/fastly/ibexa_main.vcl --main fastly vcl custom create --name=ibexa_user_hash.vcl --content=vendor/ibexa/fastly/fastly/ibexa_user_hash.vcl --version=latest fastly vcl snippet create --name="Re-Enable shielding on restart" --version=latest --priority 100 --type recv --content=vendor/ibexa/fastly/fastly/snippet_re_enable_shielding.vcl fastly service-version activate --version=latest ``` ## Quick introduction to Fastly CLI Fastly configuration is versioned, which means that when you alter the configuration, you create a new version and activate it. If needed, you can revert the configuration to one of previous versions at any point. ### List configuration versions ```bash fastly service-version list NUMBER ACTIVE LAST EDITED (UTC) 1 false 2023-07-03 10:01 2 false 2023-07-03 10:35 3 false 2023-07-03 11:00 4 false 2023-07-03 11:28 5 false 2023-07-03 10:58 6 false 2023-07-03 11:59 7 false 2023-07-03 12:13 8 true 2023-07-03 12:13 ``` In the example above, version 8 is used (ACTIVE=true). ### Create new configuration version A version that is ACTIVE cannot be modified. To change the configuration, you need to create a new version: Clone the current active version: ```bash fastly service-version clone --version=active ``` Clone a particular version: ```bash fastly service-version clone --version=4 ``` Clone the newest version: ```bash fastly service-version clone --version=latest ``` > **Note: Command parameters** > > Most Fastly CLI commands have the `--version` parameter. In addition to a specific version number, the `--version` parameter always supports aliases like `active` and `latest`. > > Most Fastly CLI commands that alter the config also support the `--autoclone` parameter. With such commands, when you use the `--autoclone` parameter, calling `fastly service-version clone` is no longer needed. ### Activate version Activate a version with this command: ```bash fastly service-version activate --version=latest ``` ## View and modify VCL configuration Fastly configuration is stored in Varnish Configuration Language (VCL) files. You can change the behaviour of Fastly by [uploading custom VCL files](https://www.fastly.com/documentation/guides/full-site-delivery/fastly-vcl/working-with-custom-vcl/). Cohesivo ships with two VCL files that need to be enabled for Fastly to work correctly with the platform; `ibexa_main.vcl` and `ibexa_user_hash.vcl` (located in `vendor/ibexa/fastly/fastly/`) ### List custom `.vcl` files for specific version ```bash fastly vcl custom list --version 77 SERVICE ID VERSION NAME MAIN 4SEKDky8P3wdrctwZCi1C1 77 ibexa_main.vcl true 4SEKDky8P3wdrctwZCi1C1 77 ibexa_user_hash.vcl false ``` ### Get ibexa_main.vcl for specific version ```bash fastly vcl custom describe --name=ibexa_main.vcl --version=77 Service ID: 4SEKDky8P3wdrctwZCi1C1 Service Version: 77 Name: ibexa_main.vcl Main: true Content: include "ibexa_user_hash.vcl" sub vcl_recv { (....) ``` ### Provide description for specific version For each version, you can provide a description that explains what changed in that version: ```bash fastly service-version update --version=52 --comment="Added support for basic-auth on the staging domain" ``` #### List descriptions for all versions You can list the descriptions by adding the `--verbose` (`-v`) option to the `service-version list` command: ```bash fastly service-version list -v Fastly API token provided via FASTLY_API_TOKEN Fastly API endpoint: https://api.fastly.com Service ID (via FASTLY_SERVICE_ID): KlUh0J1fnw1JY1aEQ0up Versions: 8 Version 1/8 Number: 1 Comment: Initial config Service ID: KlUh0J1fnw1JY1aEQ0up Active: false Locked: true Deployed: false Staging: false Testing: false Created (UTC): 2023-07-03 08:50 Last edited (UTC): 2023-07-03 10:01 Version 2/8 Number: 2 Comment: Fixed name of origin Service ID: KlUh0J1fnw1JY1aEQ0up Active: false Locked: true Deployed: false Staging: false Testing: false Created (UTC): 2023-07-03 10:01 Last edited (UTC): 2023-07-03 10:35 (...) ``` ### Modify Fastly configuration You can modify the existing Fastly configuration, for example, by uploading a modified `.vcl` file. Create a new version based on the one that is currently active, and upload the file: ```bash fastly vcl custom update --name=ibexa_main.vcl --version=active --autoclone --content=vendor/ibexa/fastly/fastly/ibexa_main.vcl ``` Provide a description of the change in Fastly's version system: ```bash fastly service-version update --version=latest --comment="Added feature X" ``` Activate the new version: ```bash fastly service-version activate --version=latest ``` ## Snippets You can also add VCL code to the Fastly configuration without modifying the custom `.vcl` files directly. You do it by creating [snippets](https://www.fastly.com/documentation/guides/full-site-delivery/fastly-vcl/vcl-snippets/about-vcl-snippets/). it's recommended that you use snippets instead of changing the VCL files provided by Cohesivo as much as possible, which makes it easier to upgrade the Cohesivo VCL configuration later. When you use snippets, the snippet code is injected into the VCL where the `#FASTLY ...` macros are placed. For example, if you create a snippet for the `recv` subroutine, it's injected into the `ibexa_main.vcl` file, the line where `#FASTLY recv` is found. ### List available snippets for specific version ```bash fastly vcl snippet list --version=active SERVICE ID VERSION NAME DYNAMIC SNIPPET ID KlUh0J1fnw1JY1aEQ0up 8 Re-Enable shielding on restart false 1iJWIfsPLNGxcphsjggq ``` > **Note: Note** > > As of version 3.3.24, 4.1.6 and 4.2.0, Cohesivo also requires one snippet to be installed, in addition to the custom VCLs `ibexa_main.vcl` and `ibexa_user_hash.vcl`. That snippet is by default named `Re-Enable shielding on restart`. ### Get details of installed snippets Use the `vcl snippet list` command with the `--verbose` option to get information such as: priority, which subroutine it's attached to (for example, `vcl_recv` or `vcl_fetch`) and the code itself. ```bash fastly vcl snippet list --version=active -v Fastly API token provided via FASTLY_API_TOKEN Fastly API endpoint: https://api.fastly.com Service ID (via FASTLY_SERVICE_ID): [....] Service Version: 8 Name: Re-Enable shielding on restart ID: 1iJWIfsPLNGxcphsjggq Priority: 100 Dynamic: false Type: recv Content: // This code should be added as a snippet in your config: // Name: Re-Enable shielding on restart // Priority: 100 // Type: recv // // Fastly CLI: // - fastly vcl snippet create --name="Re-Enable shielding on restart" --version=active --autoclone --priority 100 --type recv --content=vendor/ibexa/fastly/fastly/snippet_re_enable_shielding.vcl // - fastly service-version activate --version=latest set var.fastly_req_do_shield = (req.restarts <= 2); # set var.fastly_req_do_shield = (req.restarts > 0 && req.http.accept == "application/vnd.fos.user-context-hash"); set req.http.X-Snippet-Loaded = "v1"; Created at: 2022-06-23 10:55:34 +0000 UTC Updated at: 2022-06-23 12:24:48 +0000 UTC ``` You can also get the same details for a particular snippet using the `vcl snippet describe` command. ### Get specific snippet ```bash fastly vcl snippet describe --name="Re-Enable shielding on restart" --version=latest ``` ### Create snippet ```bash fastly vcl snippet create --name="Re-Enable shielding on restart" --version=active --autoclone --priority 100 --type recv --content=vendor/ibexa/fastly/fastly/snippet_re_enable_shielding.vcl fastly service-version activate --version=latest ``` ### Update existing snippet ```bash fastly vcl snippet update --name="Re-Enable shielding on restart" --version=active --autoclone --priority 100 --type recv --content=vendor/ibexa/fastly/fastly/snippet_re_enable_shielding.vcl fastly service-version activate --version=latest ``` ### Delete snippet ```bash fastly vcl snippet delete --name="Re-Enable shielding on restart" --version=active --autoclone fastly service-version activate --version=latest ``` ### Get diff between two versions You can view the diff between two different versions by using the Fastly web interface. Unfortunately, Fastly CLI doesn't support this functionality. However, Fastly API and GNU diff can help you get an identical result. Use the Fastly API to download the generated `.vcl` file. It includes the VCL configuration that Fastly generates based on all the configuration settings (from all custom `.vcl` files, snippets, and origin configuration). The example below extracts the generated VCL for version no. 11 of some service: ```bash curl -i "https://api.fastly.com/service/[FASTLY_SERVICE_ID]/version/11/generated_vcl" -H "Fastly-Key: [FASTLY_API_TOKEN]" -H "Accept: application/json" > generated_vcl_11_raw cp generated_vcl_11_raw generated_vcl_11_json_only ``` Next, you need to edit `generated_vcl_11_json_only` in your favourite editor, remove anything before the json data and save. Then, follow the same steps again for version no. 12 (or whatever version you want to diff version 11 against). Then replace `\n` in the files to get human-readable diffs: ```bash cat generated_vcl_11_json_only |jq .content|perl -pe 's/\\n/\n/g' > generated_vcl_11_json_done cat generated_vcl_12_json_only |jq .content|perl -pe 's/\\n/\n/g' > generated_vcl_12_json_done ``` Finally, you can use GNU diff to get a readable diff of the two versions: ```bash diff -ruN generated_vcl_11_json_done generated_vcl_12_json_done ``` ## Enable basic-auth on Fastly To enable basic-auth, use [Fastly documentation](https://www.fastly.com/documentation/solutions/examples/http-basic-auth/) as an example. Follow the steps below. Usernames and passwords can be stored inside the VCL file, but in this case credentials are stored in a [dictionary](https://www.fastly.com/documentation/guides/full-site-delivery/dictionaries/working-with-dictionaries/#working-with-dictionaries-using-vcl-snippets). > **Note: Note** > > To make this example work, you must run Cohesivo in version 3.3.16 or later, or 4.5. ### Create and activate dictionary Fastly configuration includes a dictionary named `basicauth`. Using a dictionary instead of storing usernames directly in a `.vcl` file is beneficial, because you can add or remove records without having to create and activate new configuration versions. ```bash fastly dictionary create --version=active --autoclone --name=basicauth fastly service-version activate --version=latest ``` ### Get dictionary ID To add users to the dictionary, first get the dictionary ID. ```bash fastly dictionary list --version=active Service ID: KlUh0J1fnw1JY1aEQ0up Version: 3 ID: ltC6Rg4pqw4qaNKF5tEW Name: basicauth Write Only: false Created (UTC): 2023-07-03 10:33 Last edited (UTC): 2023-07-03 10:33 ``` In the example above, the ID is `ltC6Rg4pqw4qaNKF5tEW`. ### Create record in dictionary Add username and password to the dictionary: ```bash fastly dictionary-entry create --dictionary-id=ltC6Rg4pqw4qaNKF5tEW --key=user1 --value=foobar1 ``` ### List dictionary records You can list the records from a dictionary by using the following command: ```bash fastly dictionary-entry list --dictionary-id=ltC6Rg4pqw4qaNKF5tEW33 ``` Now your dictionary stores new username and password. The next thing to do is to alter the Fastly VCL configuration and add the basic-auth support. This example uses [snippets](https://www.fastly.com/documentation/guides/full-site-delivery/fastly-vcl/vcl-snippets/about-vcl-snippets/), so that no changes are needed in the `.vcl` files that are shipped with Cohesivo. You need two snippets, store these as files in your system: In `snippet_basic_auth_error.vcl`: ```bash // This code should be added as a snippet in your config: // Name: BasicAuth error // Priority: 100 // Type: error // See snippet_basic_auth_recv.vcl for installation instructions // # If status code is a 401, a synthetic HTML page with this error is served to the user. if (obj.status == 401) { set obj.http.Content-Type = "text/html; charset=utf-8"; set obj.http.WWW-Authenticate = "Basic realm=MYREALM"; synthetic {" Error

    401 Unauthorized (Fastly)

    "}; return (deliver); } ``` In `snippet_basic_auth_recv.vcl`: ```bash // This code should be added as a snippet in your config: // Name: BasicAuth recv // Priority: 100 // Type: recv // // Fastly CLI: // - fastly vcl snippet create --name="BasicAuth recv" --version=active --autoclone --priority 100 --type recv --content=snippet_basic_auth_recv.vcl // - fastly vcl snippet create --name="BasicAuth error" --version=latest --priority 100 --type error --content=snippet_basic_auth_error.vcl // - fastly service-version activate --version=latest declare local var.credential STRING; declare local var.username STRING; declare local var.password STRING; declare local var.result STRING; # Basic auth is checked on edge nodes only. The logic below makes sure that it's only run at the edge. if (fastly.ff.visits_this_service == 0 && req.restarts == 0) { if (req.http.Authorization ~ "(?i)^Basic ([a-z0-9_=]+)$") { set var.credential = digest.base64_decode(re.group.1); set var.username = if(var.credential ~ "^(.+?):.+$", re.group.1, ""); set var.password = if(var.credential ~ "^.+?:(.+)$", re.group.1, ""); set var.result = table.lookup(basicauth, var.username, "NOTFOUND"); if (var.result == "NOTFOUND") { error 401 "Restricted"; } else if (var.result != var.password) { error 401 "Restricted"; } # The Auth header is unset to avoid exposing it as a response header. unset req.http.Authorization; set req.http.Auth-User = var.username; } else { error 401 "Restricted"; } } # Unsetting req.http.Authorization to avoid reaching "return(pass)" in vcl_recv for the first ESI request if (req.is_esi_subreq) { unset req.http.Authorization; } ``` To enable basic-auth for one domain only, alter `snippet_basic_auth_recv.vcl`: ```diff -if (fastly.ff.visits_this_service == 0 && req.restarts == 0 &&) { +if (fastly.ff.visits_this_service == 0 && req.restarts == 0 && req.http.host == "example.com") { ``` Install the snippets with the following Fastly CLI command: ```bash fastly vcl snippet create --name="BasicAuth recv" --version=active --autoclone --priority 100 --type recv --content=snippet_basic_auth_recv.vcl fastly vcl snippet create --name="BasicAuth error" --version=latest --priority 100 --type error --content=snippet_basic_auth_error.vcl fastly service-version activate --version=latest ``` # Persistence cache > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). Persistence cache caches SPI\Persistence calls used in common page loads. ![SPI cache diagram](https://doc.ibexa.co/en/saas/infrastructure_and_maintenance/img/spi_cache.png) ## Layers Persistence cache can best be described as an implementation of `SPI\Persistence` that decorates the main backend implementation, aka Storage Engine *(currently: "Legacy Storage Engine")*. As shown in the illustration, this is done in the exact same way as the event layer is a custom implementation of `API\Repository` decorating the main repository. In the case of persistence cache, instead of sending events on calls passed on to the decorated implementation, most of the load calls are cached, and calls that perform changes purge the affected caches. Cache handlers *(for example, Redis, or Filesystem)* can be configured using Symfony configuration. For details on how to reuse this Cache service in your own custom code, see below. ## Transparent cache With the persistence cache, like with the HTTP cache, Cohesivo follows the principles of transparent caching. The cache is invisible to the end user (admin/editors) of Cohesivo and content is always returned *fresh*. ## What is cached? Persistence cache aims at caching most `SPI\Persistence` calls used in common page loads, including everything needed for permission checking and URL alias lookups. Notes: - [Cache tagging](https://symfony.com/doc/7.4/components/cache/cache_invalidation.html#using-cache-tags) is used in order to allow clearing cache by alternative indexes. For instance tree operations or changes to content types are examples of operations that also need to invalidate content cache by tags. - Search isn't defined as persistence and the queries themselves aren't planned to be cached as they're too complex by design (for example, full text). Use [Solr](https://doc.ibexa.co/en/saas/search/search_engines/solr_search_engine/solr_overview/index.md) which caches this for you to improve scale/performance, and to offload your database. For further details on which calls are cached or not, see details in the [Symfony Web Debug Toolbar](https://doc.ibexa.co/en/saas/infrastructure_and_maintenance/devops/#web-debug-toolbar) which has info on cache use in two places: - Symfony Cache tab: for Symfony Cache itself, the tab shows cache lookups to cache backends - Ibexa tab: shows calls made to database back end, and if they're cached or not To see where and how to contribute additional caches, refer to the [source code](https://github.com/ibexa/core/blob/6.0/src/lib/Persistence/Cache/Readme.md). ## Persistence cache configuration > **Note: Note** > > Current implementation uses [Symfony application cache](https://symfony.com/doc/7.4/cache.html#system-cache-and-application-cache). It technically supports the following cache backends: [APCu, Array, Chain, Doctrine, Filesystem, PDO & Doctrine DBAL, Php Array, Proxy, Redis](https://symfony.com/doc/7.4/cache.html#available-cache-adapters). Cohesivo officially supports only using Filesystem for single server and Redis/Valkey for clustered setups. Use of [Redis/Valkey](#redisvalkey) as shared cache backend is a requirement for use in clustering setup. For an overview of this feature, see [Clustering](https://doc.ibexa.co/en/saas/infrastructure_and_maintenance/clustering/clustering/index.md). Filesystem adapters, for example, are **not** intended to be used over a shared filesystem. ### Cache service The underlying cache system is exposed as an `ibexa.cache_pool` service, and can be reused by any other service as described in the [Using Cache service](#using-cache-service) section. By default, configuration uses the `cache.tagaware.filesystem` service to store cache files. The service is defined in `config/packages/cache_pool/cache.tagaware.filesystem.yaml` to use [FilesystemTagAwareAdapter](https://github.com/ibexa/recipes/blob/master/ibexa/oss/4.0/config/packages/cache_pool/cache.tagaware.filesystem.yaml#L8). You can select a different cache backend and configure its parameters in the relevant file in the `cache_pool` folder. ### Multi repository setup You can [configure multisite to work with multiple Repositories](https://doc.ibexa.co/en/saas/administration/configuration/repository_configuration/#defining-custom-connection). Then, in configuration you can specify which cache pool you want to use on a SiteAccess or SiteAccess group level. The following example shows use in a SiteAccess group: ```yaml ibexa: system: # "site_group" refers to the group configured in site access site_group: # cache_pool is set to '%env(CACHE_POOL)%' # env(CACHE_POOL) is set to 'cache.tagaware.filesystem' (a Symfony service) by default, for more examples see config/packages/cache_pool/* cache_service_name: '%cache_pool%' ``` > **Note: One cache pool for each repository** > > If your installation has several repositories *(databases)*, make sure every group of sites that uses different repositories also uses a different cache pool. ### In-Memory cache configuration Persistence cache layer caches selected objects in-memory for a short time. It avoids loading repeatedly the same data from, for example, a remote Redis instance, which can take up to 4-5ms per call due to the network latency and Redis instance load. The cache is organized in 2 pools, one for metadata which isn't updated frequently, and one for content related objects that is only meant as a short-lived burst cache. Limit is organized using a [least frequently used (LFU)](https://en.wikipedia.org/wiki/Least_frequently_used) approach. It makes sure repeatedly used objects stay in-memory until expired, and those seldom used are bulk evicted from cache every time the maximum number of cache items is reached. This in-memory cache is purged *(for the current PHP process)* when clearing it using any of the mentioned methods below. For other processes, the object is refreshed when it expires or evicted when it reaches the cache limits. In-Memory cache is configured globally, and has the following default settings: ```yaml parameters: # Config for metadata cache pool, here showing default config # ttl: Maximum number of milliseconds objects are kept in-memory (3000ms = 3s) ibexa.spi.persistence.cache.inmemory.ttl: 3000 # limit: Maximum number of cache objects to place in-memory, to avoid consuming too much memory ibexa.spi.persistence.cache.inmemory.limit: 100 # enabled: Is the in-memory cache enabled ibexa.spi.persistence.cache.inmemory.enable: true # Config for content cache pool, here showing default config ## WARNING: TTL is on purpose low to avoid getting outdated data in prod! For dev environment, you can safely increase it (e.g. by x3) ibexa.spi.persistence.cache.inmemory.content.ttl: 300 ibexa.spi.persistence.cache.inmemory.content.limit: 100 ibexa.spi.persistence.cache.inmemory.content.enable: true ``` > **Caution: In-Memory cache is per-process** > > **TTL and Limit need to have a low value.** Setting limit high increases memory use. High TTL value also increases exponentially risk for system acting on stale metadata (for example, content type definitions). The only case where it's safe to increase these values is for dev environment with single concurrency on writes. In prod environment you should only consider reducing them if you have heavy concurrency writes. ### Redis/Valkey [Redis](https://redis.io/), an in-memory data structure store, is one of the supported cache solutions for clustering. Redis is used via [Redis PECL extension](https://pecl.php.net/package/redis). See [Redis Cache Adapter in Symfony documentation](https://symfony.com/doc/7.4/components/cache/adapters/redis_adapter.html#configure-the-connection) for information on how to connect to Redis. [Valkey](https://valkey.io/), an alternative data structure store compatible with Redis, is also supported. To set it up with Cohesivo, follow the same steps as for Redis. #### Supported Adapters There are two Redis adapters available out of the box that fit different needs. ##### `Symfony\Component\Cache\Adapter\RedisTagAwareAdapter` **Requirement**: Redis server configured with eviction [`maxmemory-policy`](https://redis.io/docs/latest/develop/reference/eviction/#eviction-policies): `volatile-ttl`, `volatile-lru` or `volatile-lfu` (Redis 4.0+). Use of LRU or LFU is recommended. it's also possible to use `noeviction`, but it's usually not practical. **Pros**: It's typically faster than `RedisAdapter`, because fewer lookups needed to cache backend. **Cons**: Consumes much more memory. To avoid situations where Redis stops accepting new cache (warnings about `Failed to save key`), set aside enough memory for the Redis server. ##### `Symfony\Component\Cache\Adapter\RedisAdapter` **Pros**: Uses a bit less memory than `RedisTagAwareAdapter`, so it eliminated the risk of stopping saving cache when there isn't enough memory. **Cons**: 1.5-2x more lookups to the back-end cache server then `RedisTagAwareAdapter`. Depending on the number of lookups and latency to cache server this might affect page load time. #### Adjusting configuration A default example that you can use out-of-the-box is found in `config/packages/cache_pool/cache.redis.yaml`. For anything else, you can enable it with environment variables. For instance, if you set the following environment variables `export CACHE_POOL="cache.redis" CACHE_DSN="secret@example.com:1234/13"`, it results in config like this: ```yaml services: cache.redis: # NOTE: Available via https://github.com/symfony/cache class: Symfony\Component\Cache\Adapter\RedisTagAwareAdapter parent: cache.adapter.redis tags: - name: cache.pool clearer: cache.app_clearer provider: 'redis://secret@example.com:1234/13' # Default CACHE_NAMESPACE value, see config/cache_pool/cache.redis.yaml for usage with e.g. multi repo. namespace: 'ezp' ``` See `.env`, `config/packages/ibexa.yaml` and `config/packages/cache_pool/cache.redis.yaml` for further details on `CACHE_POOL`, `CACHE_DSN` and `CACHE_NAMESPACE`. > **Caution: Clearing Redis cache** > > The regular `php bin/console cache:clear` command doesn't clear Redis persistence cache. Use a dedicated Symfony command to clear the pool you have configured: `php bin/console cache:pool:clear cache.redis`. ##### Redis clustering Persistence cache depends on all involved web servers, each of them seeing the same view of the cache because it's shared among them. With that in mind, the following configurations of Redis are possible: - [Redis Cluster](https://redis.io/docs/latest/operate/oss_and_stack/management/scaling/) - Shards cache across several instances to be able to cache more than memory of one server allows - Shard slaves can improve availability, however [they use asynchronous replication](https://redis.io/docs/latest/operate/oss_and_stack/management/scaling/#redis-cluster-consistency-guarantees) so they can't be used for reads - Unsupported Redis features that can affect performance: [pipelining](https://github.com/phpredis/phpredis/blob/develop/cluster.md#pipelining) and [most multiple key commands](https://github.com/phpredis/phpredis/blob/develop/cluster.md#multiple-key-commands) - [Redis Sentinel](https://redis.io/docs/latest/operate/oss_and_stack/management/sentinel/) - Provides high availability by providing one or several slaves (ideally 2 slaves or more, for example, minimum 3 servers), and handle failover - [Slaves are asynchronously replicated](https://redis.io/docs/latest/operate/oss_and_stack/management/sentinel/#fundamental-things-to-know-about-sentinel-before-deploying), so they can't be used for reads - Typically used with a load balancer (for example, HAProxy with occasional calls to Redis Sentinel API) in the front to only speak to elected master - As of v3 you can also configure this [directly on the connection string](https://symfony.com/doc/7.4/components/cache/adapters/redis_adapter.html#configure-the-connection), **if** you use `Predis` instead of `php-redis` Several cloud providers have managed services that are easier to set up, handle replication and scalability for you, and might perform better. Notable services include: - [Amazon ElastiCache](https://aws.amazon.com/elasticache/) - [Azure Redis Cache](https://azure.microsoft.com/en-us/products/cache/) - [Google Cloud Memorystore](https://cloud.google.com/memorystore) ###### Ibexa Cloud usage > **Note: Ibexa Cloud** > > If you use Upsun Enterprise you can benefit from the Redis Sentinel across three nodes for great fault tolerance. Upsun Professional and lower versions offer Redis in single instance mode only. ## Using cache service Using the internal cache service allows you to use an interface and without caring whether the system is configured to place the cache in Redis or on File system. And as Cohesivo requires that instances use a cluster-aware cache in cluster setup, you can safely assume your cache is shared *(and invalidated)* across all web servers. > **Note: Note** > > Current implementation uses a caching library implementing TagAwareAdapterInterface which extends `Psr\Cache\CacheItemPoolInterface`, and therefore is compatible with PSR-6. > **Caution: Use unique vendor prefix for Cache key** > > When reusing the cache service within your own code, it's very important to not conflict with the cache keys used by others. That is why the example of usage below starts with a unique `myApp` key. For the namespace of your own cache, you must do the same. ### Getting cache service #### With dependency injection In your Symfony services configuration you can inject the cache service in your configuration like so: ```yaml # yml configuration App\MyService: arguments: - '@ibexa.cache_pool' ``` This service is an instance of `Symfony\Component\Cache\Adapter\TagAwareAdapterInterface`, which extends the `Psr\Cache\CacheItemPoolInterface` interface with tagging functionality. ### Using the cache service Example usage of the cache service: ```php use Symfony\Component\Cache\Adapter\TagAwareAdapterInterface; /** * @var TagAwareAdapterInterface $pool * @var int $id */ $cacheItem = $pool->getItem("myApp-object-{$id}"); if ($cacheItem->isHit()) { return $cacheItem->get(); } $myObject = $myAppCustomService->loadObject($id); $cacheItem->set($myObject); $cacheItem->tag(['myApp-category-' . $myObject->categoryId]); $pool->save($cacheItem); return $myObject; ``` For more info on usage, see [Symfony Cache's documentation](https://symfony.com/doc/7.4/cache.html). ### Clearing persistence cache Persistence cache uses the `ibx-` prefix, declared as a container parameter called `ibexa.core.persistence.cache.tag_prefix`. You can clear the cache as in the following example: ```php use Symfony\Component\Cache\Adapter\TagAwareAdapterInterface; /** * @var TagAwareAdapterInterface $pool * @var int $contentId */ // To clear all cache (not recommended without a good reason) $pool->clear(); // To clear a specific cache item (check source for more examples in Ibexa\Core\Persistence\Cache\*) $pool->deleteItems(["ibx-ci-$contentId"]); // Symfony cache is tag-based, so you can clear all cache related to a content item like this: $pool->invalidateTags(["c-$contentId"]); ``` To learn how to clear persistence cache when not using the PHP API, see [Clear persistence cache](https://doc.ibexa.co/en/saas/infrastructure_and_maintenance/devops/#clear-persistence-cache). # Clustering > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). Clustering enables you to host one installation of Cohesivo on multiple servers. Clustering in Cohesivo refers to setting up your installation with several web servers for handling more load and/or for failover. ## Server setup overview This diagram illustrates how clustering in Cohesivo is typically set up. The parts illustrate the different roles needed for a successful cluster setup. ![Server setup for clustering](https://doc.ibexa.co/en/saas/infrastructure_and_maintenance/img/server_setup.png) The number of web servers, Redis/Valkey, Solr, Varnish, Database, and NFS servers, but also whether some servers play several of these roles (typically running Redis/Valkey across the web server), is up to you and your performance needs. The minimal requirements are: - [Shared HTTP cache (using Varnish)](https://doc.ibexa.co/en/saas/infrastructure_and_maintenance/cache/http_cache/reverse_proxy/#using-varnish-or-fastly) - [Shared persistence cache](#shared-persistence-cache) and [sessions](#shared-sessions) (using Redis/Valkey) - Shared database (using MySQL/MariaDB) - [Shared binary files](#shared-binary-files) (using NFS, or S3) It's also recommended to use: - [Solr](https://doc.ibexa.co/en/saas/search/search_engines/solr_search_engine/solr_overview/index.md) or [Elasticsearch](https://doc.ibexa.co/en/saas/search/search_engines/elasticsearch/elasticsearch_overview/index.md) for better search and performance - a CDN for improved performance and faster ping time worldwide - you can use Fastly, which has native support as HTTP cache and CDN. - active/passive database for failover - more recent versions of PHP and MySQL/MariaDB supported by your Cohesivo version to get more performance out of each server. Numbers might vary so make sure to test this when upgrading. ### Shared persistence cache Redis and Valkey are the recommended cache solutions for clustering. See [persistence cache documentation](https://doc.ibexa.co/en/saas/infrastructure_and_maintenance/cache/persistence_cache/#persistence-cache-configuration) on information on how to configure them. ### Shared sessions For a [cluster](https://doc.ibexa.co/en/saas/infrastructure_and_maintenance/clustering/clustering/index.md) setup you need to configure sessions to use a back end that is shared between web servers. The main option out of the box in Symfony is the PHP Redis session handler (also compatible with Valkey). Alternatively, there is Symfony session handler for PDO (database). To avoid concurrent access to session data from front-end nodes, if possible you should either: - Enable [Session locking](https://www.php.net/manual/en/features.session.security.management.php#features.session.security.management.session-locking) - Use "Sticky Session", aka [Load Balancer Persistence](https://en.wikipedia.org/wiki/Load_balancing_%28computing%29#Persistence) Session locking is available with `php-redis` (v4.2.0 and higher). On Ibexa Cloud (and Upsun) Redis and Valkey are preferred and supported. ### Shared binary files Cohesivo supports multi-server setups by means of [custom IO handlers](https://doc.ibexa.co/en/saas/content_management/file_management/file_management/#dfs-cluster-handler). They make sure that files are correctly synchronized among the multiple clients using the data. ## DFS IO handler The DFS IO handler (`legacy_dfs_cluster`) can be used to store binary files on an NFS server. It uses a database to manipulate metadata, making up for the potential inconsistency of network-based filesystems. ### Configuring the DFS IO handler You need to configure both metadata and binarydata handlers. Cohesivo ships with a custom local adapter (`ibexa.io.nfs.adapter.site_access_aware`), which decorates the Flysystem v2 local adapter to enable support for SiteAccess-aware settings. If an NFS path relies on SiteAccess-aware dynamic parameters, you must use the custom local adapter instead of the Flysystem v2 local adapter. Configure the custom local adapter to read/write to the NFS mount point on each local server. As metadata handler, create a DFS one, configured with a Doctrine connection. > **Tip: Tip** > > The default database install now includes the dfs table *in the same database* First, define DFS folder path as a variable in `.env` file: `DFS_NFS_PATH=` Next, if you're using a separate DFS database, configure it via the `DATABASE_URL` variable in the `.env` file. Depending on which database you're using: `DFS_DATABASE_URL=mysql://root:rootpassword@127.0.0.1:3306/ibexa_dfs?serverVersion=8.0` or `DATABASE_URL=postgresql://root:rootpassword@127.0.0.1:5432/ibexa_dfs?serverVersion=14.18` For production, it's recommended to create the DFS table in its own database, manually importing its schema definition: > **Note: dfs_schema.sql (MySQL)** > > ```sql > CREATE TABLE ibexa_dfs_file ( > name text NOT NULL, > name_trunk text NOT NULL, > name_hash varchar(34) NOT NULL DEFAULT '', > datatype varchar(255) NOT NULL DEFAULT 'application/octet-stream', > scope varchar(25) NOT NULL DEFAULT '', > size bigint(20) unsigned NOT NULL DEFAULT '0', > mtime int(11) NOT NULL DEFAULT '0', > expired tinyint(1) NOT NULL DEFAULT '0', > status tinyint(1) NOT NULL DEFAULT '0', > PRIMARY KEY (name_hash), > KEY ibexa_dfs_file_name (name (191)), > KEY ibexa_dfs_file_name_trunk (name_trunk (191)), > KEY ibexa_dfs_file_mtime (mtime), > KEY ibexa_dfs_file_expired_name (expired,name (191)) > ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; > ``` > **Note: dfs_schema.sql (PostgreSQL)** > > ```sql > CREATE TABLE ibexa_dfs_file ( > name_hash varchar(34) DEFAULT '' NOT NULL, > name text NOT NULL, > name_trunk text NOT NULL, > datatype varchar(255) DEFAULT 'application/octet-stream' NOT NULL, > scope character varying(25) DEFAULT '' NOT NULL, > size bigint DEFAULT 0 NOT NULL, > mtime integer DEFAULT 0 NOT NULL, > expired boolean DEFAULT false NOT NULL, > status boolean DEFAULT false NOT NULL > ); > > ALTER TABLE ONLY ibexa_dfs_file > ADD CONSTRAINT ibexa_dfs_file_pkey PRIMARY KEY (name_hash); > > CREATE INDEX ibexa_dfs_file_expired_name ON ibexa_dfs_file USING btree (expired, name); > CREATE INDEX ibexa_dfs_file_mtime ON ibexa_dfs_file USING btree (mtime); > CREATE INDEX ibexa_dfs_file_name ON ibexa_dfs_file USING btree (name); > CREATE INDEX ibexa_dfs_file_name_trunk ON ibexa_dfs_file USING btree (name_trunk); > ``` This example uses Doctrine connection named `dfs`: ```yaml parameters: env(DFS_DATABASE_URL): '%env(resolve:DATABASE_URL)%' dfs_database_url: '%env(resolve:DFS_DATABASE_URL)%' ibexa.io.nfs.adapter.config: root: '%kernel.project_dir%/%env(string:DFS_NFS_PATH)%' path: '$var_dir$/$storage_dir$/' writeFlags: ~ linkHandling: ~ permissions: [ ] # new Doctrine connection for the DFS legacy_dfs_cluster metadata handler. doctrine: dbal: connections: dfs: # configure these for your database server driver: '%env(string:DFS_DATABASE_DRIVER)%' charset: '%env(string:DFS_DATABASE_CHARSET)%' default_table_options: charset: '%env(string:DFS_DATABASE_CHARSET)%' collate: '%env(string:DFS_DATABASE_COLLATION)%' url: '%env(string:DFS_DATABASE_URL)%' # define the Flysystem handler oneup_flysystem: adapters: nfs_adapter: custom: service: ibexa.io.nfs.adapter.site_access_aware # define the Ibexa handlers ibexa_io: binarydata_handlers: nfs: flysystem: adapter: nfs_adapter metadata_handlers: dfs: legacy_dfs_cluster: connection: doctrine.dbal.dfs_connection # set the application handlers ibexa: system: default: io: metadata_handler: dfs binarydata_handler: nfs ``` > **Tip: Tip** > > If you're looking to [set up S3](https://doc.ibexa.co/en/saas/infrastructure_and_maintenance/clustering/clustering_with_aws_s3/index.md) or other [Flysystem](https://flysystem.thephpleague.com/docs/)/third-party adapters like Google Cloud Storage, this needs to be configured as binary handler. The rest here still stays the same, the DFS metadata handler takes care of caching the lookups to avoid slow IO lookups. #### Customizing the storage directory Earlier versions required the NFS adapter directory to be set to `$var_dir$/$storage_dir$` part for the NFS path. It's no longer required, but the default prefix used to serve binary files still matches this expectation. If you decide to change this setting, make sure you also set `io.url_prefix` to a matching value. If you set the NFS adapter's directory to `/path/to/nfs/storage`, use this configuration so that the files can be served by Symfony: ```yaml ibexa: system: default: io: url_prefix: storage ``` As an alternative, you may serve images from NFS by using a dedicated web server. If in the example above, this server listens on `http://static.example.com/` and uses `/path/to/nfs/storage` as the document root, configure `io.url_prefix` as follows: ```yaml ibexa: system: default: io: url_prefix: 'http://static.example.com/' ``` You can read more about that on [Binary files URL handling](https://doc.ibexa.co/en/saas/content_management/file_management/file_url_handling/#file-url-handling). ### Web server rewrite rules The default Cohesivo rewrite rules let image requests be served directly from disk. In a cluster setup, files matching `^/var/([^/]+/)?storage/images(-versioned)?/.*` have to be passed through `/public/index.php` instead. In any case, this specific rewrite rule must be placed before the ones that "ignore" image files and let the web server serve the files directly. #### Apache ```apacheconf RewriteRule ^/var/([^/]+/)?storage/images(-versioned)?/.* /index.php [L] ``` Place this before the standard image rewrite rule in your vhost config (or uncomment if already there). #### nginx ```nginx rewrite "^/var/([^/]+/)?storage/images(-versioned)?/(.*)" "/index.php" break; ``` Place this before the include of `ibexa_params.d`/`ibexa_rewrite_params` in your vhost config (or uncomment if already there). ## Migrating to a cluster setup If you're migrating an existing single-server site to a cluster setup, and not setting up clustering from scratch, you need to migrate your files. Once you have configured your binarydata and metadata handlers, you can run the `ibexa:io:migrate-files` command. You can also use it when you're migrating from one data handler to another, for example, from NFS to Amazon S3. This command shows which handlers are configured: ```bash > php bin/console ibexa:io:migrate-files --list-io-handlers Configured meta data handlers: default, dfs, aws_s3 Configured binary data handlers: default, nfs, aws_s3 ``` You can do the actual migration like this: ```bash php bin/console ibexa:io:migrate-files --from=default,default --to=dfs,nfs --env=prod ``` The `--from` and `--to` values must be specified as `,`. If `--from` is omitted, the default IO configuration is used. If `--to` is omitted, the first non-default IO configuration is used. > **Tip: Tip** > > The command must be executed with the same permissions as the web server. While the command is running, the files should not be modified. To avoid surprises you should create a [backup](https://doc.ibexa.co/en/saas/infrastructure_and_maintenance/backup/index.md) and/or execute a dry run before doing the actual update, using the `--dry-run` switch. Since this command can run for a long time, to avoid memory exhaustion, use the `--env=prod` switch when you run it in the production environment. # Clustering with Amazon AWS S3 > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). When you're using a clustering configuration, you can store binary files on Amazon AWS S3. When setting up clustering, you can use Amazon AWS S3 as a binary handler, meaning AWS S3 is used to store binary files. > **Tip: Tip** > > Before you start, you should be familiar with the [clustering](https://doc.ibexa.co/en/saas/infrastructure_and_maintenance/clustering/clustering/index.md) documentation. ## Set up AWS S3 account 1. Go to  and create an account. An [existing Amazon account can be used](https://docs.aws.amazon.com/AmazonS3/latest/userguide/GetStartedWithS3.html#sign-up-for-aws). 2. [Choose a region](https://docs.aws.amazon.com/storagegateway/latest/vgw/available-regions-intro.html). The example below uses EU (Ireland): `eu-west-1` 3. Create a bucket in your chosen region and make note of the bucket name: . 4. Go to the [IAM Management Console](https://console.aws.amazon.com/iam/home#/users) and create a user. See . 5. Then create a group and assign the user to the group. 6. Assign policies to the group. The `AmazonS3FullAccess` policy gives read/write access to your bucket. 7. Still in the IAM console, view the user you created. Click the **Security credentials** tab. 8. Click "Create access key" and make note of the "Access key ID" and the "Secret access key". The secret key cannot be retrieved again after the key has been created, so don't lose it. (However, you can create new keys if needed.) > **Note: Note** > > Make sure that your bucket is [configured as Public](https://docs.aws.amazon.com/AmazonS3/latest/userguide/configuring-block-public-access-bucket.html) to avoid facing 403 errors, as the current S3 handler is meant to store files publicly so they can be served directly from S3. ## Set up Cohesivo for AWS S3 In your Cohesivo root directory, run `php composer require league/flysystem-aws-s3-v3:^2.0`. Then, register the AWS S3 client as a service: ```yaml services: Aws\S3\S3Client: arguments: - version: latest region: eu-west-1 # The region string of your chosen region credentials: key: ABCDEF... # Your AWS key ID secret: abc123... # Your AWS secret key ``` Set up the Flysystem v2 adapter that uses the S3 client under the `oneup_flysystem.adapters` [configuration key](https://doc.ibexa.co/en/saas/administration/configuration/configuration/#configuration-files): ```yaml oneup_flysystem: adapters: aws_s3_adapter: awss3v3: client: Aws\S3\S3Client bucket: my-bucket # Your bucket name prefix: 'my-prefix' # Your custom prefix, for example: 'my_site' ``` In the same place, set up the binary data handler for the S3 adapter: ```yaml ibexa_io: binarydata_handlers: aws_s3: flysystem: adapter: aws_s3_adapter ``` > **Note: Note** > > `aws_s3` is an arbitrary handler identifier that is used in the config block below. You can configure multiple handlers. > > For example, you could configure one called `gcloud_storage` for a [Google Cloud Storage adapter](https://github.com/thephpleague/flysystem#officially-supported-adapters). Under the `ibexa.system..io` [configuration key](https://doc.ibexa.co/en/saas/administration/configuration/configuration/#configuration-files), enable the binary data handler: ```yaml ibexa: system: default: io: binarydata_handler: aws_s3 # Also remember to use DFS for metadata_handler to avoid expensive lookups to S3 (see Clustering guide) # metadata_handler: dfs ``` Clear all caches and reload, and that's it. ## Migrate your existing binary data to S3 You can [migrate existing binary data](https://doc.ibexa.co/en/saas/infrastructure_and_maintenance/clustering/clustering/#migrating-to-a-cluster-setup) to S3 with the `php bin/console ibexa:io:migrate-files` command. # DevOps > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). See various tools that can help you debug your Cohesivo installation. ## Cache clearing Cohesivo contains multiple layer of caching that you can clear independently from each other: - [Clear system cache](#clear-system-cache) - [Clear persistence cache](#clear-persistence-cache) - [Clear HTTP cache](#clear-http-cache) ### Clear system cache [System cache](https://symfony.com/doc/7.4/cache.html#system-cache-and-application-cache) is separate for every [Symfony environment](https://doc.ibexa.co/en/saas/infrastructure_and_maintenance/environments/index.md) and stores information derivable from source code like compiled container, routes, or optimized classes. To clear the system cache, execute the `cache:clear` command on [every web server](https://doc.ibexa.co/en/saas/infrastructure_and_maintenance/clustering/clustering/index.md) running Cohesivo. To specify an environment, pass it by using either the `--env` option or the `APP_ENV` variable. Both the examples below clear the system cache for the `prod` environment: - `APP_ENV=prod php bin/console cache:clear` - `php bin/console cache:clear --env=prod` When neither the `--env` option nor the `APP_ENV` variable is set, `cache:clear` clears the system cache for the `dev` environment by default. Don't run `cache:clear` as root as it can lead to issues with file ownership. > **Caution: Symfony 7.4 behavior change** > > Starting with Symfony 7.4, running `php bin/console cache:clear` or `rm -rf var/cache/*` clears only the system cache, even when you use a filesystem-based cache pool for [persistence cache](#clear-persistence-cache). You must always clear the persistence cache separately. #### Clearing system cache manually During development, you can clear the system cache manually by running: ```bash rm -rf var/cache/* ``` > **Caution: Don't clear system cache manually on production** > > Manually clearing the system cache doesn't warm up the cache, resulting in a significant performance drop on the first request. To avoid this, you must not clear the cache manually in a production environment. ### Clear persistence cache [Persistence cache](https://doc.ibexa.co/en/saas/infrastructure_and_maintenance/cache/persistence_cache/index.md) stores information about application data. To clear the persistence cache, you must run: ```bash php bin/console cache:pool:clear ``` The default cache pool is named `cache.tagaware.filesystem`. The default cache pool when running Redis or Valkey is named `cache.redis`. If you customized the persistence cache configuration, the name of your cache pool might be different. #### Clearing persistence cache manually During development, when using a filesystem-based cache pool, you can clear the application cache by running: ```bash rm -rf var/share/* ``` ### Clear HTTP cache [HTTP cache](https://doc.ibexa.co/en/saas/infrastructure_and_maintenance/cache/http_cache/http_cache/index.md) uses reverse proxies like Varnish or Fastly to store application responses controlled by [HTTP Cache headers](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Cache-Control). To clear the HTTP cache, see [Purging from command line](https://doc.ibexa.co/en/saas/infrastructure_and_maintenance/cache/http_cache/content_aware_cache/#purging-from-command-line). ## Web Debug Toolbar As of Cohesivo v4.5, the [Symfony Web Debug Toolbar](https://symfony.com/doc/7.4/profiler.html) is no longer installed by default. To install it, run the following command: ```bash composer require --dev symfony/debug-pack ``` After you have installed Symfony Web Debug Toolbar, it's available when running Cohesivo in the `dev` environment. It's extended with some Cohesivo-specific information: ![Cohesivo info in Web Debug Toolbar](https://doc.ibexa.co/en/saas/infrastructure_and_maintenance/img/web_debug_toolbar.png "Cohesivo info in Web Debug Toolbar") ### SPI (persistence) This section provides the number of non-cached SPI calls and handlers. You can see details of these calls in the [Symfony Profiler](https://symfony.com/doc/7.4/profiler.html) page. ### SiteAccess Here you can see the name of the current SiteAccess and how it was matched. For reference see the [list of possible SiteAccess matchers](https://doc.ibexa.co/en/saas/multisite/siteaccess/siteaccess_matching/#available-siteaccess-matchers). ## Logging and debug configuration Logging in Cohesivo consists of two parts. One are several debug systems that integrate with Symfony developer toolbar to give you detailed information about what is going on. The other is the standard [PSR-3](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-3-logger-interface.md) logger, as provided by Symfony using [Monolog](https://github.com/Seldaek/monolog). ### Debugging in dev environment When using the Symfony `dev` [environment](https://doc.ibexa.co/en/saas/infrastructure_and_maintenance/environments/index.md), the system tracks additional metrics for you to be able to debug issues. They include Symfony cache use, and a [persistence cache](https://doc.ibexa.co/en/saas/infrastructure_and_maintenance/cache/persistence_cache/#persistence-cache-configuration) use. #### Reducing memory use > **Tip: Tip** > > For long-running scripts, see [Long-running console commands](https://doc.ibexa.co/en/saas/infrastructure_and_maintenance/performance/#long-running-console-commands). If you're running out of memory and don't need to keep track of cache hits and misses, you can disable persistence cache logging, represented by the setting `parameters.ibexa.spi.persistence.cache.persistenceLogger.enableCallLogging`. In `config_dev.yaml`: ```yaml parameters: ibexa.spi.persistence.cache.persistenceLogger.enableCallLogging: false ``` ### Error logging and rotation Cohesivo uses the [Monolog](https://github.com/Seldaek/monolog) component to log errors, and it has a `RotatingFileHandler` that allows for file rotation. According to [their documentation](https://seldaek.github.io/monolog/doc/02-handlers-formatters-processors.html#log-to-files-and-syslog), it "logs records to a file and creates one logfile per day. It also deletes files older than `$maxFiles`". Monolog's handler can be configured in `config/packages//monolog.yaml`: ```yaml monolog: handlers: main: type: rotating_file max_files: 10 path: '%kernel.logs_dir%/%kernel.environment%.log' level: debug ``` ### Using `logrotate` Monolog themselves recommend using [`logrotate`](https://manpages.debian.org/jessie/logrotate/logrotate.8.en.html) instead of doing the rotation in the handler, because it gives better performance. # Backup > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). Periodically back up your Repository information by making a database backup. You should always make sure that your solution is properly backed up. The following example shows you how to do this on a Linux-UNIX-based system. You should shut down Cohesivo if it's running before making a backup. > **Note: Externally stored assets** > > If you store assets in any external service or localization, you should back them up before proceeding. 1. Navigate into the Cohesivo directory: ```bash cd /path/to/ibexa ``` 2. Clear all caches: ```bash rm -rf var/cache/* rm -rf var/share/* rm -rf var/logs/* ``` 3. Create a dump of the database: **MySQL** ```bash mysqldump -u --add-drop-table > db_backup.sql ``` **PostgreSQL** ```bash pg_dump -c --if-exists > db_backup.sql ``` 4. In parent directory create a tar archive of the files (including the database dump) using the "tar" command: ```bash tar cfz backup_of_ibexa.tar.gz ibexa ``` At this point, the file `backup_of_ibexa.tar.gz` should contain a backup of database and files. # Performance > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). Ensure that your Cohesivo installation performs well by following our set of recommendations. Cohesivo can be set up to run efficiently on almost any modern configuration. What follows is a list of recommendation that make your installation perform better. > **Note: Note** > > All the following recommendations are valid for both development and production setups, unless otherwise noted. If you're in a hurry, the most important recommendations on this page are: - Dump optimized Composer autoload classmap - Use a full web (Nginx/Apache) server with vhost - Avoid shared filesystems for code (Docker for Mac/Win, VirtualBox/\*, Vagrant, and more), or find ways to optimize or work around the issues. - For clustering (mainly relevant for production/staging), reduce latency to Redis/Valkey, use Varnish and [Solr](https://doc.ibexa.co/en/saas/search/search_engines/solr_search_engine/solr_overview/index.md). ## Client - Always use an up-to-date browser and an up-to-date operating system so you have access to latest browser versions - If possible, use a fast, stable internet connection, because an unreliable connection slows down UI ## Server In production setups: - Always use reverse proxy, and if possible use Varnish. - Compared to the built-in Symfony Proxy in PHP Varnish is much faster and is able to queue up requests for the same fresh/invalidated resource. - With [ibexa/http-cache](https://github.com/ibexa/http-cache) support for xkey and grace Varnish provides more stable performance in read/write scenarios. - Set up Cohesivo in [cluster mode](https://doc.ibexa.co/en/saas/infrastructure_and_maintenance/clustering/clustering/index.md) if you need to handle bigger spikes of traffic than a single server can manage. - See [recommendation for Redis-compatible data stores](#redis-compatible-data-stores) and [Search](#search) below. > **Note: Note** > > The following recommendations are ordered from largest to smallest impact they have on performance in general. ### VM - Avoid shared filesystems for code (for example, Docker for Mac/Win, VirtualBox/\*, or Vagrant), because they typically slow down the application 10x or more, compared to native Linux filesystem. - VM in itself also adds 10-30% of overhead. However when it comes to production, for example, AWS vs barebones, it also comes down to cost and convenience factors. ### Web server - Use Nginx/Apache even for development, as PHP's built-in web server (as exposed via Symfony's `server:*` commands) is only able to handle one request at a time (including JS/CSS/\* asset loading, and more). - Use a recent version of nginx, set up https, and enable http/2 to reduce connection latency on parallel requests. ### PHP - Always enable opcache for php-fpm/`mod_php`. - Prefer php-fpm and web server using it over fast-cgi for lower overall memory usage. ### Symfony - Review the [Symfony performance documentation](https://symfony.com/doc/7.4/performance.html) and apply matching suggestions, including OPCache configuration if enabled. ### Frontend assets Deploy a [production build](https://webpack.js.org/guides/production/) of your assets to reduce their size and improve loading time. You can build them by running `yarn encore prod`, or by setting the environmental variable `NODE_ENV` to `production` in your production environment. ### Composer - Keep Composer up to date. - Always dump optimized class map using `composer dump-autoload --optimize` or relevant flags on `composer install/update`. ### Redis-compatible data stores - Redis and its alternatives, like Valkey, can in some cases perform better than filesystem cache even with a single server, as it offers better general performance for operations invalidating cache. - However, pure read performance is slower, especially if the next points aren't optimized. - With cache being on different node(s) than web server, make sure to try to tune latency between the two. > **Tip: Tip** > > Check if your cloud provider has native service for Redis, as those might be better tuned. When using Redis or Valkey, make sure to tune it for in-memory cache usage. The persistence feature isn't needed with cache and severely slows down execution time. [For use with sessions](https://doc.ibexa.co/en/saas/infrastructure_and_maintenance/sessions/#cluster-setup) however, persistence can be a good fit if you want sessions to survive service interruptions. For more information, see [Redis clustering](https://doc.ibexa.co/en/saas/infrastructure_and_maintenance/cache/persistence_cache/#redis-clustering). ### Search - Use [Solr Bundle and Solr](https://doc.ibexa.co/en/saas/search/search_engines/solr_search_engine/solr_overview/index.md) to greatly offload your database and get more stable performance on your installation. ## Long-running console commands Executing long-running console commands can result in running out of memory. Two examples of such commands are a custom import command and the indexing command provided by the [Solr Bundle](https://doc.ibexa.co/en/saas/search/search_engines/solr_search_engine/solr_overview/index.md). ### Reducing memory usage To avoid quickly running out of memory while executing such commands you should make sure to: 1. Always run in prod environment using: `--env=prod` 1. See [Environments](https://doc.ibexa.co/en/saas/infrastructure_and_maintenance/environments/index.md) for further information on Symfony environments. 2. See [Logging and debug configuration](https://doc.ibexa.co/en/saas/infrastructure_and_maintenance/devops/#logging-and-debug-configuration) for some of the different features enabled in development environments, which by design use memory. 2. For logging using monolog, if you use either the default `fingers_crossed`, or `buffer` handler, make sure to specify `buffer_size` to limit how large the buffer grows before it gets flushed: ```yaml # config_prod.yaml (partial example) monolog: handlers: main: type: fingers_crossed buffer_size: 200 ``` 3. Run PHP without memory limits: `php -d memory_limit=-1 bin/console ` 4. Disable `xdebug` *(PHP extension to debug/profile php use)* when running the command, this causes php to use much more memory. > **Note: Memory still grows** > > Even when everything is configured like described above, memory grows for each iteration of indexing/inserting a content item with at least *1kb* per iteration after the initial first 100 rounds. This is expected behavior. To be able to handle more iterations you have to do one or several of the following: > > - Change the import/index script in question to [use process forking](#process-forking-with-symfony) to avoid the issue. > - Upgrade PHP: *newer versions of PHP are typically more memory-efficient.* > - Run the console command on a machine with more memory (RAM). ### Process forking with Symfony The recommended way to completely avoid "memory leaks" in PHP in the first place is to use processes. For console scripts this is typically done using process forking which is achievable with Symfony. The things you need to do: 1. Change your command so it supports taking slice parameters, like for instance a batch size and a child-offset parameter. 1. *If defined, child-offset parameter denotes if a process is a child, this can be accomplished using two commands as well.* 2. *If not defined, it's the master process which executes the processes until nothing is left to process.* 2. Change the command so that the master process takes care of forking child processes in slices. 1. For execution in-order, [you may look to our platform installer code](https://github.com/ibexa/core/blob/6.0/src/bundle/RepositoryInstaller/Command/InstallPlatformCommand.php#L220) used to fork out Solr indexing after installation to avoid cache issues. 2. For parallel execution of the slices, [see Symfony doc for further instruction](https://symfony.com/doc/7.4/components/process.html#process-signals). # Background tasks > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). Use Ibexa Messenger to run processes in the background and conserve system resources. Some operations in Cohesivo don’t have to run immediately when a user clicks a button, for example, re-indexing product prices or processing bulk data. Running such operations in real time could slow down the system and disrupt the user experience. To solve this, Cohesivo provides a package called Ibexa Messenger, which is an overlay to [Symfony Messenger](https://symfony.com/doc/7.4/messenger.html), and it's job is to queue tasks and run them in the background. Cohesivo sends messages (or commands) that represent the work to be done later. These messages are stored in a queue and picked up by a background worker, which ensures that resource-heavy tasks are executed at a convenient time, without putting excessive load on the system. Ibexa Messenger supports multiple storage backends, such as Doctrine, Redis/Valkey, and PostgreSQL, and gives developers the flexibility to create their own message handlers for custom use cases. ## How it works Ibexa Messenger uses a command bus as a queue that stores messages, or commands, which tell the system what you want to happen, and separates them from the handler, which is the code that actually performs the task. The process works as follows: 1. A message PHP object is dispatched, for example, `ProductPriceReindex`. 2. The message is wrapped in an envelope, which may contain additional metadata, called [stamps](#stamps). 3. The message is placed in the [transport queue](#route-message-to-background-queue). It can be a Doctrine table, a Redis/Valkey queue, and so on. 4. A worker process continuously reads messages from the queue, pulls them into the default bus `ibexa.messenger.bus` and assigns them to the right handler. 5. A handler service processes the message (executes the command). You can register multiple handlers for different jobs. Here is an example of how you can extend your code and use Ibexa Messenger to process your tasks: ### Configure package Create a config file, for example, `config/packages/ibexa_messenger.yaml` and define your transport: ```yaml ibexa_messenger: # The DSN of the transport, as expected by Symfony Messenger transport factory. transport_dsn: 'doctrine://default?table_name=ibexa_messenger_messages&auto_setup=false' deduplication_lock_storage: enabled: true # Doctrine DBAL primary connection or custom service type: doctrine # One of "doctrine"; "custom"; "service" # The service ID of a custom Lock Store, if "service" type is selected service: null # The DSN of the lock store, if "custom" type is selected dsn: null ``` > **Note: Supported transports** > > You can define different transports: Ibexa Messenger has been tested to work with Redis, MySQL, PostgreSQL. For more information, see [Symfony Messenger documentation](https://symfony.com/doc/current/messenger.html#transports-async-queued-messages) or [Symfony Messenger tutorial](https://symfonycasts.com/screencast/messenger/install). ### Start worker Use a process manager of your choice to run the following command, or make it start together with the server: ```bash php bin/console messenger:consume ibexa.messenger.transport --bus=ibexa.messenger.bus --siteaccess= ``` Use the `--siteaccess` option to set the default [SiteAccess](https://doc.ibexa.co/en/saas/multisite/multisite_configuration/#siteaccess-configuration) and [repository](https://doc.ibexa.co/en/saas/administration/configuration/repository_configuration/#defining-custom-connection) for the worker process. The worker uses this SiteAccess for every message that doesn't have a [`SiteAccessStamp`](#siteaccessstamp). If a message has a `SiteAccessStamp`, the worker uses the SiteAccess from the stamp instead to process this message. Thanks to this, one worker process can handle messages coming from different SiteAccesses. In [multi-repository setups](https://doc.ibexa.co/en/saas/administration/configuration/repository_configuration/index.md), run one worker process for each repository. With this setup, each worker process can connect to the right database. > **Caution: Multi-repository setups** > > Doctrine transport works across multiple repositories without issues, but other transports may need to be adjusted, so that queues across different repositories are not accidentally shared. #### Configure for production environment In production, make sure that Ibexa Messenger keeps running. You can configure a process manager, such as [Supervisor](https://symfony.com/doc/7.4/messenger.html#messenger-supervisor) or [systemd](https://symfony.com/doc/7.4/messenger.html#systemd-configuration), to restart the worker if it stops. To prevent issues with memory leaks or stale processes, run the worker with execution limits: - `--limit` limits the number of messages the worker processes before exiting. - `--time-limit` limits the execution time in seconds before the worker exits. - `--memory-limit` restricts the maximum memory usage. The following example shows how you can specify these limits: ```bash php bin/console messenger:consume ibexa.messenger.transport --bus=ibexa.messenger.bus --limit=100 --time-limit=60 --memory-limit=256M ``` For more information, see [Symfony production recommendation for the Messenger component](https://symfony.com/doc/7.4/messenger.html#deploying-to-production). If you deploy your application on Ibexa Cloud, using [Workers](https://fixed.docs.upsun.com/guides/symfony/workers.html) is recommended. ## Dispatch message To have a task processed in the background by Ibexa Messenger: 1. Inject the `ibexa.messenger.bus` service as an object implementing the `Symfony\Component\Messenger\MessageBusInterface` interface. 2. Dispatch an appropriate message, for example a [custom message](#register-custom-message-and-handler), by using the `MessageBusInterface::dispatch()` method, exactly as described in [Symfony Messenger documentation](https://symfony.com/doc/7.4/messenger.html#dispatching-the-message). ```yaml services: SomeClassThatSchedulesExecutionInTheBackground: arguments: $bus: '@ibexa.messenger.bus' ``` ```php bus->dispatch(new SomeMessage()); } } ``` 3. [Route the message to the background queue](#route-message-to-background-queue). Otherwise the bus calls the handler immediately, in the same process that dispatches the message. 4. Additionally, attach message metadata by using [stamps](#stamps). ### Stamps You can attach [Stamps](https://symfony.com/doc/7.4/messenger.html#envelopes-stamps) to a message envelope to add additional metadata and control processing of the message. The `ibexa.messenger.bus` message bus uses the default Symfony Messenger [middleware](https://symfony.com/doc/7.4/messenger.html#middleware) and doesn't support all stamps that are available in Symfony. You can use the following Symfony stamps: - [`DeduplicateStamp`](https://symfony.com/doc/7.4/messenger.html#message-deduplication) - [`DelayStamp`](https://github.com/symfony/symfony/blob/7.4/src/Symfony/Component/Messenger/Stamp/DelayStamp.php) - [`DispatchAfterCurrentBusStamp`](https://symfony.com/doc/7.4/messenger.html#dispatchaftercurrentbusmiddleware-middleware) - [`HandlerArgumentsStamp`](https://symfony.com/doc/7.4/messenger.html#additional-handler-arguments) - [`SerializerStamp`](https://symfony.com/doc/7.4/messenger.html#serializing-messages) On top of the supported Symfony stamps, Cohesivo provides the following ones: - [`SudoStamp`](#sudostamp) - [`UserPermissionStamp`](#userpermissionstamp) - [`SiteAccessStamp`](#siteaccessstamp) #### SudoStamp [`SudoStamp`](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-Messenger-Stamp-SudoStamp.html) causes the handler to [use sudo mode](https://doc.ibexa.co/en/saas/api/php_api/php_api/#using-sudo), bypassing all permission checks when processing the message. The following example shows how you can attach the `SudoStamp` to the message: ```php use App\Message\SomeMessage; use Ibexa\Contracts\Messenger\Stamp\SudoStamp; use Symfony\Component\Messenger\MessageBusInterface; /** @var MessageBusInterface $bus */ $bus->dispatch(new SomeMessage(), [new SudoStamp()]); ``` #### UserPermissionStamp [`UserPermissionStamp`](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-Messenger-Stamp-UserPermissionStamp.html) allows you to [set the repository user](https://doc.ibexa.co/en/saas/api/php_api/php_api/#setting-the-repository-user) to process the message. When the user is set, handlers execute actions on their behalf and take their permissions into account. If you don't attach this stamp, the messages are processed by the default repository user called anonymous user. By combing this stamp with [`SudoStamp`](#sudostamp), you can set the repository user and skip the permission checks at the same time. The following example shows how you can use `UserPermissionStamp` to preserve the current repository user after the message is dispatched. ```php use App\Message\SomeMessage; use Ibexa\Contracts\Core\Repository\PermissionResolver; use Ibexa\Contracts\Messenger\Stamp\UserPermissionStamp; use Symfony\Component\Messenger\MessageBusInterface; /** @var PermissionResolver $permissionResolver */ $currentUserId = $permissionResolver->getCurrentUserReference()->getUserId(); /** @var MessageBusInterface $bus */ $bus->dispatch(new SomeMessage(), [new UserPermissionStamp($currentUserId)]); ``` #### SiteAccessStamp [`Ibexa\Contracts\Messenger\Stamp\SiteAccessStamp`](https://doc.ibexa.co/en/saas/api/php_api/php_api_reference/classes/Ibexa-Contracts-Messenger-Stamp-SiteAccessStamp.html) contains the name of the [SiteAccess](https://doc.ibexa.co/en/saas/multisite/siteaccess/siteaccess/index.md) that dispatched the message. You don't need to add this stamp manually, Ibexa Messenger attaches this stamp to each dispatched message automatically. The stamp contains the SiteAccess that is current at the moment of dispatch. Before the worker calls the handler, it changes the configuration scope to the SiteAccess from the stamp. The handler then reads [SiteAccess-aware configuration](https://doc.ibexa.co/en/saas/multisite/multisite_configuration/#siteaccess-configuration) for the SiteAccess that dispatched the message, and not for the SiteAccess that the worker process started with. > **Caution: The stamp doesn't change the current SiteAccess** > > The stamp changes the configuration scope only. It doesn't change the SiteAccess in the `Ibexa\Core\MVC\Symfony\SiteAccess\SiteAccessServiceInterface` service. `SiteAccessServiceInterface::getCurrent()` always returns the SiteAccess that the worker process started with, for all messages. > > To get a SiteAccess-aware value in a handler, use the [`ConfigResolverInterface` service](https://doc.ibexa.co/en/saas/administration/configuration/dynamic_configuration/index.md). ## Extend Ibexa Messenger To handle a custom use case with background tasks, you need the following elements: - a message class to hold the data - a handler class to perform the task, registered on the `ibexa.messenger.bus` bus - a message provider to [route the message to the transport queue](#route-message-to-background-queue) - code to [dispatch the message](#dispatch-message) If you don't route the message to the transport queue, the bus calls the handler synchronously, in the same process that dispatches the message. ### Register custom message and handler To handle additional use cases with background tasks, first create a [custom message and handler class](https://symfony.com/doc/7.4/messenger.html#creating-a-message-handler): ```php For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). In Cohesivo you can use environment provided by Symfony in virtual host configuration, and to create custom environments. Environment configuration is provided by Symfony. Cohesivo additionally enables you to specify environments in virtual host configuration. You can configure several environments, such as production, development, or staging. You can have different configuration sets for each of them. > **Tip: Tip** > > See also [Environments in Symfony doc](https://symfony.com/doc/7.4/configuration.html#configuration-environments). ## Web server configuration For example, when you use Apache, in the [`VirtualHost` example](https://raw.githubusercontent.com/ibexa/post-install/6.0/resources/templates/apache2/vhost.template) in your installation, the required `VirtualHost` configurations have been already included. You can switch to the desired environment by setting the `ENVIRONMENT` variable to `prod`, `dev` or another custom value, like in the following example: ```apacheconf # Environment. # Possible values: "prod" and "dev" out-of-the-box, other values possible with proper configuration # Defaults to "prod" if omitted (uses SetEnvIf so value can be used in rewrite rules) SetEnvIf Request_URI ".*" APP_ENV="dev" ``` ## Custom environments If you want to use a custom environment (something other than `prod` and `dev`), you need to place dedicated configuration files in a separate folder: `config/packages//config_.yaml` The name used as `` is the one that can be used as value of the `ENVIRONMENT` variable. This enables you to override settings defined in the main configuration file, depending on your environment (for example database settings). # Sessions > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). Cohesivo uses Symfony to handle user sessions, with support for SiteAccess-aware session cookie configuration. Sessions are handled by the Symfony framework, specifically API and underlying session handlers provided by the HttpFoundation component. It's further enhanced in Cohesivo with support for SiteAccess-aware session cookie configuration. > **Note: Note** > > Use of Redis, Valkey, or experimentally PDO as session handler is a requirement in a cluster setup, for details [see below](#cluster-setup). For an overview of the clustering feature see [Clustering](https://doc.ibexa.co/en/saas/infrastructure_and_maintenance/clustering/clustering/index.md). ## Configuration Symfony offers the possibility to change many session options at application level (for example, in Symfony [`framework` configuration](https://symfony.com/doc/7.4/reference/configuration/framework.html#session)). These options include: - `cookie_domain` - `cookie_path` - `cookie_lifetime` - `cookie_secure` - `cookie_httponly` However, in Cohesivo you can set up several sites within one Symfony application, so you can also define session configuration per SiteAccess and SiteAccess group level. ### Session options per SiteAccess All site-related session configuration can be defined per SiteAccess and SiteAccess group under the `ibexa.system..session` [configuration key](https://doc.ibexa.co/en/saas/administration/configuration/configuration/#configuration-files): ```yaml ibexa: system: my_siteaccess: session: # Default session name is IBX_SESSION_ID{siteaccess_hash} # (unique session name per SiteAccess) name: my_session_name # These are optional.  # If not defined they will fall back to Symfony framework configuration,  # which itself falls back to default php.ini settings cookie_domain: mydomain.com cookie_path: /foo cookie_lifetime: 86400 cookie_secure: false cookie_httponly: true ``` ## Session handlers In Symfony, a session handler is configured with `framework.session.handler_id`. Symfony can be configured to use custom handlers, or fall back to what is configured in PHP by setting it to null (`~`). ### Default configuration Cohesivo adapts Symfony's defaults to make sure its session save path is always taken into account: ```yaml # Default session configuration framework: session: # handler_id can be set to null (~) like default in Symfony, if it so will use default session handler from php.ini # But in order to use %ibexa.session.save_path%, default Cohesivo instead sets %ibexa.session.handler_id% to: # - session.handler.native_file (default) # - Ibexa\Bundle\Core\Session\Handler\NativeSessionHandler (recommended value for Cluster usage, using php-redis session handler ) handler_id: '%ibexa.session.handler_id%' ``` ### Recommendations for production setup #### Single-server setup For a single server, the default file handler is preferred. #### Cluster setup See [shared sessions in the clustering guide](https://doc.ibexa.co/en/saas/infrastructure_and_maintenance/clustering/clustering/#shared-sessions). ##### Handling sessions with Redis and Valkey Cohesivo supports storing sessions with [Redis](https://pecl.php.net/package/redis) or [Valkey](https://valkey.io/) data stores. To set it up, you need to: - [Configure the session save handler settings in `php.ini`](https://github.com/phpredis/phpredis/blob/6.2.0/README.md#php-session-handler) - Set `%ibexa.session.handler_id%` to `~` *(null)* in `config/packages/ibexa.yaml` Alternatively if you have needs to configure the servers dynamically: - Set `%ibexa.session.handler_id%` (or `SESSION_HANDLER_ID` env var) to `Ibexa\Bundle\Core\Session\Handler\NativeSessionHandler` - Set `%ibexa.session.save_path%` (or `SESSION_SAVE_PATH` env var) to [`save_path` config for Redis](https://github.com/phpredis/phpredis/blob/6.2.0/README.md#php-session-handler) If you're on `php-redis` v4.2.0 and higher, you can optionally tweak [`php-redis` settings](https://github.com/phpredis/phpredis/blob/6.2.0/README.md#session-locking) for session locking. Ideally keep [persistence cache](https://doc.ibexa.co/en/saas/infrastructure_and_maintenance/cache/persistence_cache/index.md) and session data separated: - Sessions can't risk getting [randomly evicted](https://redis.io/docs/latest/develop/reference/eviction/#eviction-policies) when you run out of memory for cache. - You can't completely disable eviction either, as the data store then starts to refuse new entries once full, including new sessions. - Either way, you should monitor your data store instances and make sure you have enough memory set aside for active sessions/cache items. If you want to make sure sessions survive data store or server restarts, consider setting up [persistent storage](https://redis.io/docs/latest/operate/oss_and_stack/management/persistence/) instance for sessions. ##### Alternative storing sessions in database by using PDO For setups where database is preferred for storing sessions, you may use Symfony's PdoSessionHandler, although it's not currently recommended from performance perspective. Below is a configuration example for Cohesivo. Refer to the [Symfony Cookbook](https://symfony.com/doc/7.4/session.html#session-database-pdo) for full documentation. ```yaml framework: session: # ... handler_id: session.handler.pdo parameters: pdo.db_options: db_table: session db_id_col: session_id db_data_col: session_value db_time_col: session_time services: PDO: arguments: dsn: 'mysql:dbname=' user: password: Symfony\Component\HttpFoundation\Session\Storage\Handler\PdoSessionHandler: arguments: ['@pdo', '%pdo.db_options%'] ``` # Development security > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). Ensure the security of your Cohesivo installation by using one of the available authentication methods. > **Tip: Tip** > > See [Permissions](https://doc.ibexa.co/en/saas/permissions/permissions/index.md) for information about the permissions system in Cohesivo. > **Note: Security checklist** > > See the [Security checklist](https://doc.ibexa.co/en/saas/infrastructure_and_maintenance/security/security_checklist/index.md) for a list of security-related issues you should take care of before going live with a project. ## Symfony authentication To use Symfony authentication with Cohesivo, use the following configuration (in `config/packages/security.yaml`): ```yaml security: firewalls: ibexa_front: pattern: ^/ user_checker: Ibexa\Core\MVC\Symfony\Security\UserChecker anonymous: ~ form_login: require_previous_session: false logout: ~ ``` And in `config/routes.yaml`: ```yaml login: path: /login defaults: { _controller: Ibexa\Core\MVC\Symfony\Controller\SecurityController::loginAction } login_check: path: /login_check logout: path: /logout ``` > **Note: Note** > > You can fully customize the routes and/or the controller used for login. However, remember to match `login_path`, `check_path` and `logout.path` from `security.yaml`. > > See [security configuration reference](https://symfony.com/doc/7.4/reference/configuration/security.html) and [standard login form documentation](https://symfony.com/doc/7.4/security.html#form-login). ### Authentication using Symfony Security component Authentication is provided by the Symfony Security component. [Native and universal `form_login`](https://symfony.com/doc/7.4/security.html#form-login) is used, in conjunction with an extended `DaoAuthenticationProvider` (DAO stands for *Data Access Object*), the `RepositoryAuthenticationProvider`. Native behavior of `DaoAuthenticationProvider` has been preserved, making it possible to still use it for pure Symfony applications. #### Security controller A `SecurityController` is used to manage all security-related actions and is thus used to display the login form. It follows all standards explained in [Symfony security documentation](https://symfony.com/doc/7.4/security.html#form-login). The base template used is [`Security/login.html.twig`](https://github.com/ibexa/core/blob/6.0/src/bundle/Core/Resources/views/Security/login.html.twig). The layout used by default is `%ibexa.content_view.viewbase_layout%` (empty layout) but can be configured together with the login template: ```yaml ibexa: system: my_siteaccess: user: layout: layout.html.twig login_template: user/login.html.twig ``` ##### Redirection after login By default, Symfony redirects to the [URI configured in `security.yaml` as `default_target_path`](https://symfony.com/doc/7.4/reference/configuration/security.html). If not set, it defaults to `/`. #### Remember me It's possible to use the "Remember me" functionality. Refer to the [Symfony cookbook on this topic](https://symfony.com/doc/7.4/security/remember_me.html). If you want to use this feature, you must at least extend the login template to add the required checkbox: ```html+twig {% extends "@IbexaCore/Security/login.html.twig" %} {% block login_fields %} {{ parent() }} {% endblock %} ``` #### Login handlers / SSO Symfony provides native support for [multiple user providers](https://symfony.com/doc/7.4/security/user_providers.html). This makes it easy to integrate any kind of login handlers, including SSO and existing third-party bundles (for example, [FR3DLdapBundle](https://github.com/Maks3w/FR3DLdapBundle), [HWIOauthBundle](https://github.com/hwi/HWIOAuthBundle), [FOSUserBundle](https://github.com/FriendsOfSymfony/FOSUserBundle), [BeSimpleSsoAuthBundle](https://github.com/BeSimple/BeSimpleSsoAuthBundle), and more). See [Authenticating a user with multiple user provider](https://doc.ibexa.co/en/saas/users/user_authentication/#authenticate-user-with-multiple-user-providers) for more information. ## JWT authentication To use [JWT authentication](https://www.jwt.io/) with Cohesivo, in the provided `config/packages/lexik_jwt_authentication.yaml` file, modify the existing configuration by setting `authorization_header` to `enabled`: ```yaml lexik_jwt_authentication: secret_key: '%env(APP_SECRET)%' encoder: signature_algorithm: HS256 # Disabled by default, because Page builder uses a custom extractor token_extractors: authorization_header: enabled: true cookie: enabled: false query_parameter: enabled: false ``` You also need to configure Symfony firewalls for the APIs with which you want to use JWT authentication. It's already provided in `config/packages/security.yaml`, you need to uncomment the `ibexa_jwt_rest` and the ones for the desired APIs: ```yaml security: firewalls: ibexa_jwt_rest: request_matcher: Ibexa\Rest\Security\JWTTokenCreationRESTRequestMatcher user_checker: Ibexa\Core\MVC\Symfony\Security\UserChecker stateless: true provider: ibexa json_login: check_path: ibexa.rest.create_token username_path: JWTInput.username password_path: JWTInput.password success_handler: lexik_jwt_authentication.handler.authentication_success failure_handler: lexik_jwt_authentication.handler.authentication_failure ibexa_jwt_rest.api: request_matcher: Ibexa\Rest\Security\AuthorizationHeaderRESTRequestMatcher user_checker: Ibexa\Core\MVC\Symfony\Security\UserChecker provider: ibexa stateless: true jwt: ~ ibexa_jwt_graphql: request_matcher: Ibexa\GraphQL\Security\NonAdminGraphQLRequestMatcher provider: ibexa stateless: true jwt: ~ ``` - `ibexa_jwt_rest` is the firewall that allows to generate a JWT token through REST or GraphQL - `ibexa_jwt_rest.api` is the firewall to [use JWT authentication for REST API](https://doc.ibexa.co/en/saas/api/rest_api/rest_api_authentication/#jwt-authentication) instead of session-based - `ibexa_jwt_graphql` is the firewall to [use JWT authentication for GraphQL API](https://doc.ibexa.co/en/saas/api/graphql/graphql/#jwt-authentication) For example, to use JWT authentication only for GraphQL API and keep session-based authentication for REST API: - uncomment `ibexa_jwt_rest` and `ibexa_jwt_graphql` to activate them - keep `ibexa_jwt_rest.api` commented and disabled ### Use PEM keys Out of the box, JWT tokens are created by using HMAC (Hash-based Message Authentication Code) with `APP_SECRET` as the secret key and the `HS256` (HMAC-SHA256) algorithm. You can use PEM (Privacy-enhanced Electronic Mail) keys and the `RS256` (RSA-SHA256) algorithm instead. 1. Set the `JWT_PASSPHRASE` secret In an `.env` file, you should have the following variables: ```text JWT_SECRET_KEY=%kernel.project_dir%/config/jwt/private.pem JWT_PUBLIC_KEY=%kernel.project_dir%/config/jwt/public.pem JWT_PASSPHRASE=0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ…… ``` Set your `JWT_PASSPHRASE`, its value must be strong, random, and securely stored. For more recommendations and to learn how to generate one, see [`APP_SECRET` and other secrets](https://doc.ibexa.co/en/saas/infrastructure_and_maintenance/security/security_checklist/#app_secret-and-other-secrets). 2. In `config/packages/lexik_jwt_authentication.yaml`, use the following configuration: ```yaml lexik_jwt_authentication: secret_key: '%env(resolve:JWT_SECRET_KEY)%' public_key: '%env(resolve:JWT_PUBLIC_KEY)%' pass_phrase: '%env(JWT_PASSPHRASE)%' encoder: signature_algorithm: RS256 # … ``` 3. Generate a [PEM encoded key pair](https://symfony.com/bundles/LexikJWTAuthenticationBundle/2.x/index.html#generate-the-ssl-keys) by using the following command which outputs key files in the `config/jwt` directory: ```bash php bin/console lexik:jwt:generate-keypair ``` > **Note: Ibexa Cloud** > > To store the tokens on Ibexa Cloud, define the `config/jwt` directory as a volume in the `.platform.app.yaml` file. In 3-node cluster setups, ensure that the key pair is the same on all 3 servers. You can use a network share, or use a local mount and manually copy the key pair between the servers. For more information, see [LexikJWTAuthenticationBundle configuration reference](https://symfony.com/bundles/LexikJWTAuthenticationBundle/2.x/1-configuration-reference.html). # Security checklist > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). Ensure that your Cohesivo installation is secure by following our set of recommendations. When getting ready to go live with your project for the first time, or when re-launching it, make sure that your setup is secure. > **Caution: Caution** > > Security is an ongoing process. After going live, you should pay attention to Ibexa security advisories released via [your Service portal](https://support.ibexa.co/), or via [Security advisories](https://developers.ibexa.co/security-advisories) if you're not a subscriber. ## Cohesivo ### Carefully select admin users Make sure Admin users and other privileged users who have access to System Information and setup in the back end are vetted and fully trustworthy. As administrator, you have access to full information about the system through the `setup/system_info` policy, and also to user data, role editing, and many other critical aspects. The users in your organization who have backend access must be kept up-to-date. Any user leaving the organization must be disabled without delay. If a user takes on a new role in the organization, any required role changes for them in Cohesivo must also be made as soon as possible. ### Strong passwords Enforce strong passwords for all users. This is specially important for admin accounts and other privileged users. - Never go online with admin password set to `publish` or any other default value. - Introduce password quality checks. Make sure the checks are strict enough (length/complexity). - 16 characters is a quite secure minimum length. Don't go below 10. - If using Cohesivo v4.5 or newer, enable the password rule that rejects any password which has been exposed in a public breach. > **Tip: Password rules** > > See [setting up password rules](https://doc.ibexa.co/en/saas/users/passwords/#password-rules). ### Protect against brute force attacks Consider introducing a measure against brute force login attacks, like CAPTCHA. Adjust timeout limits to your needs: When using the "forgot password" feature, a token is created which expires if the user doesn't click the password reset link that gets mailed to them in time. The time before it expires is set in the parameter `ibexa.site_access.config.default.security.token_interval_spec`. By nature this feature must be available to users before they have logged in, including would-be attackers. If an attacker uses this feature with someone else's email address, the attacker doesn't receive the email. But they could still try to guess the password reset link. That's why this interval should be as short as possible. 5 minutes is often enough. Cohesivo allows you to create and send invitations to create an account in the frontend as a customer, the back office as an employee, or the Corporate Portal as a business partner. You can send invitations to individual users or in bulk. These invitations time out according to the parameter `ibexa.site_access.config.default.user_invitation.hash_expiration_time`. This can safely be longer than the "forgot password" time, since attackers cannot generate invitations. Don't leave it longer than it needs to be, though. These timeouts are both entered as [PHP DateInterval duration strings](https://www.php.net/manual/en/dateinterval.construct.php). The forgot password feature defaults to "PT1H" (one hour). The account invitation feature defaults to "P7D" (seven days). ### Disable Varnish when using Fastly If you're using Fastly, disable Varnish. See [Security advisory: EZSA-2020-002](https://developers.ibexa.co/security-advisories/ezsa-2020-002-unauthorised-cache-purge-with-misconfigured-fastly). ### Block upload of unwanted file types The `ibexa.site_access.config.default.io.file_storage.file_type_blacklist` setting is defined in the config file `src/bundle/Core/Resources/config/default_settings.yml` in the Core bundle. It prevents uploading files that might be executed on the server, a Remote Code Execution (RCE) vulnerability. The setting lists filename extensions for files that shouldn't be uploaded. Attempting to upload files from the list results in an error message. There are also other safety measures in place, like using the web server configuration to block execution of uploaded scripts, see the next point. You should adapt this list to your needs. `svg` images are blocked because they may contain JavaScript code. If you opt to allow them, make sure you take steps to mitigate the risk. The default list of blocked file types contains: `hta htm html jar js jse pgif phar php php3 php4 php5 phps phpt pht phtml svg swf xhtm xhtml`. ### Use secure password hashing Use the most secure supported password hashing method. This is currently `bcrypt`, and it's enabled by default. ### Use secure roles and policies Use the following checklist to ensure the roles and policies are secure: - Do roles restrict read/write access to content as they should? Is read/write access to personal data, like User content items, properly restricted? - Are the roles and their use properly differentiated and restricted? Is an editor role used for everyday editorial work? - Is the admin role used only for high-level administrative work? Is the number of people with admin access properly restricted and vetted? - Should people be allowed to create new user accounts themselves? Should such accounts be enabled by default, or require vetting by admins? - Is the role of self-created new users restricted as intended? - Is there a clear role separation between the organisation's internal and external users? - Is access to user data properly restricted, in accordance with GDPR? - Is access to Form Builder uploads managed properly? Files uploaded with the Form Builder are accessible to any user by default. If this doesn't suit you, restrict access to the Form Uploads folder. ### Don't use "hide" for read access restriction The [visibility switcher](https://doc.ibexa.co/en/saas/content_management/locations/#location-visibility) is a convenient feature for withdrawing content from the frontend. It acts as a filter in the frontend by default. You can choose to respect it or ignore it in your code. It isn't permission-based, and doesn't restrict read access to content. Hidden content can be read through other means, like the REST API or GraphQL. If you need to restrict read access to a given content item, you could create a role that grants read access for a given [**Section**](https://doc.ibexa.co/en/saas/administration/content_organization/sections/index.md) or [**Object State**](https://doc.ibexa.co/en/saas/administration/content_organization/object_states/index.md), and set a different section or object State for the given content. Or use other permission-based [**Limitations**](https://doc.ibexa.co/en/saas/permissions/limitations/index.md). ### Minimize exposure Security should be a multi-layered exercise. It's wise to minimize what features you make available to the world, even if there are no known or suspected vulnerabilities in those features, and even if your content is properly protected by roles and policies. Reduce your attack surface by exposing only what you must. - If possible, make the back office unavailable on the open internet. - [Symfony FOSJsRoutingBundle](https://github.com/FriendsOfSymfony/FOSJsRoutingBundle) is required in those releases where it's included, to expose routes to JavaScript. It exposes only the required routes, nothing more. It's only required in the back office SiteAccess though, so you can consider blocking it in other SiteAccesses. You should also go through your own custom routes, and decide for each if you need to expose them or not. See the documentation on [YAML route definitions for exposure](https://github.com/FriendsOfSymfony/FOSJsRoutingBundle/blob/master/Resources/doc/usage.rst#generating-uris). - By default, a Powered-By header is set. It specifies what version of Cohesivo is running. For example, `x-powered-by: Ibexa Experience v4`. This doesn't expose anything that couldn't be detected through other means. But if you wish to obscure this, you can either omit the version number, or disable the header entirely by setting `enabled: false`. ```yaml ibexa_system_info: system_info: powered_by: # major => v4 || minor => v4.6 || none release: major # true || false enabled: false ``` - Consider whether certain interfaces must be left available on the open internet. For example: - The `/search` and `/graphql` endpoints - The REST API endpoints > **Tip: Access control** > > One way to lock down an endpoint that should not be openly available is to restrict access to logged-in users, by using the [`access_control`](https://symfony.com/doc/7.4/security/access_control.html) feature. In your YAML configuration, under the `security` key, add an entry similar to the following one, which redirects requests to a login page: > > ```yaml > security: > access_control: > - { path: ^/search, roles: ROLE_USER} > ``` ### Limit access to Code blocks The [Code block](https://doc.ibexa.co/projects/userguide/en/6.0/content_management/block_reference/#code-block) in Page Builder is designed to accept any HTML, which includes embedded JavaScript. This means that editors who have access to Code blocks could add malicious JS including [cross site scripting (XSS)](https://en.wikipedia.org/wiki/Cross-site_scripting). As site administrator, be aware of this when giving editors access to the Page Builder features, and limit that access only to trusted editors. You can [limit access to specific blocks per content type](https://doc.ibexa.co/projects/userguide/en/6.0/content_management/configure_ct_field_settings/#default-configuration-of-pages) by defining which page blocks are available to editors. ### Activate JWT authentication for MCP, REST, or GraphQL To use [MCP servers](https://doc.ibexa.co/en/saas/ai/mcp/mcp_guide/index.md), you must enable JWT authentication for them. You can also consider enabling JWT authentication for [REST](https://doc.ibexa.co/en/saas/api/rest_api/rest_api_usage/rest_api_usage/index.md) or [GraphQL](https://doc.ibexa.co/en/saas/api/graphql/graphql/index.md) APIs. For more information, see [Development security](https://doc.ibexa.co/en/saas/infrastructure_and_maintenance/security/development_security/#jwt-authentication). ## Symfony ### `APP_SECRET` and other secrets `APP_SECRET` needs to be a strong, random, securely stored value. This applies also to other secrets that may be in use, like the Varnish invalidate token, the JWT passphrase, and any other application-specific secrets. - Don't use a default value like `ff6dc61a329dc96652bb092ec58981f7` or `ThisTokenIsNotSoSecretChangeIt`. - The secret must be secured against unwanted access. Don't commit the value to a version control system. There are several ways of handling it, like with environment variables or files like `.env.local`. Files are considered more secure. If you store the secrets in files, make sure to add those files to `.gitignore` or similar, so they will never be committed to version control systems. - The secret must be long enough. 32 characters is minimum, longer is better. > **Tip: Tip** > > The following command generates a 64-character-long secure random value: > > ```bash > php -r "print bin2hex(random_bytes(32));" > ``` > **Note: Note** > > On Ibexa Cloud, if `APP_SECRET` isn't set, the system sets it to [`PLATFORM_PROJECT_ENTROPY`](https://fixed.docs.upsun.com/guides/symfony/environment-variables.html#symfony-environment-variables) ### Symfony production mode Only expose Symfony production mode openly on the internet. Don't expose the dev mode on the internet, otherwise you may disclose things like `phpinfo` and environment variables. For more information about securing Symfony-based systems, see [Authentication and authorisation](https://symfony.com/doc/7.4/security.html), [more on this subject](https://symfony.com/doc/7.4/security.html#learn-more), and [secrets management system](https://symfony.com/doc/7.4/configuration/secrets.html), all from Symfony. ## PHP ### Enable `zend.exception_ignore_args` in PHP 7.4 and newer PHP 7.4 introduced the `zend.exception_ignore_args` setting in `php.ini`. The default value is 0 (disabled) for backwards compatibility. On production sites, this should be set to 1 (enabled) to ensure that stack traces don't include arguments passed to functions. Such arguments could include passwords or other sensitive information. You should also make sure that no stack trace is ever visible to end users of production sites. Visible arguments are unsafe even if the stack traces only show up in log files. ### Disable error output from PHP Symfony in production mode prevents exception messages from being visible to end users. However, if Symfony fails to boot properly, such exceptions may end up being visible, including stack traces. This can be prevented by [disabling error message output in PHP](https://www.php.net/manual/en/language.errors.basics.php). The following `php.ini` configuration values should be used on production sites. When using Ibexa Cloud, the same settings can be configured in Cohesivo's `.platform.app.yaml` file. ```ini display_errors = Off display_startup_errors = Off ``` ### Other PHP settings Consider what other security related settings are relevant for your needs. The [OWASP PHP Configuration Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/PHP_Configuration_Cheat_Sheet.html) contains several recommendations. For more information, see [PHP's own security manual](https://www.php.net/manual/en/security.php). ## Web server ### Block execution of scripts in `var` directory Make sure that the web server blocks the execution of PHP files and other scripts in the `var` directory. In your web server's virtual host configuration, see the line below `# Disable .php(3) and other executable extensions in the var directory`. ### Security headers There are a number of security related HTTP response headers that you can use to improve your security. Headers must be adapted to the site in question, and in most cases it's site owner's responsibility. The headers can be set either by the web server, or by a proxy like Varnish. You can also set headers in PHP code by making a Symfony `RequestListener` for the `kernel.response` event and adding the header to the response object headers list. You most likely need to vary the security headers based on the SiteAccess in question and site implementation details, such as frontend code and libraries used. - `Strict-Transport-Security` - ensures that all requests are sent over HTTPS, with no fallback to HTTP. All production sites should use HTTPS and this header unless they have particular needs. This header is less important during development provided that the site is on an internal, protected network. - `X-Frame-Options` - ensures that the site isn't embedded in a frame by a compliant browser. Set the header to `SAMEORIGIN` to allow embedding by your own site, or `DENY` to block framing completely. - `X-Content-Type-Options` - prevents the browser from second-guessing the mime-type of delivered content. This header is less important if users cannot upload content and/or you trust your editors. However, it's safer to use it at all times. Make sure that the `Content-Type` header is also correctly set, including for the top-level document, to avoid issues with HTML documents being downloaded while they should be rendered. - `Content-Security-Policy` - blocks cross site scripting (XSS) attacks by setting an allowlist (whitelist) of resources to be loaded for a given page. You can set separate lists for scripts, images, fonts, and more. For experimentation and testing, you can use `Content-Security-Policy-Report-Only` before activating the actual policy. - `Referrer-Policy` - limits what information is sent from the previous page or site when navigating to a new page or site. This header has several directives for fine-tuning the referrer information. - `Permissions-Policy` - limits what features the browser can use, such as fullscreen, notifications, location, camera, or microphone. For example, if someone succeeds in injecting their JavaScript into your site, this header prevents them from using those features to attack your users. ### Disable weak cipher suites in TLS Consider blocking the use of TLS 1.2 and older versions. The newer TLS 1.3 doesn't include the weaker cipher suites that are included in 1.2 and older. Removing them means that attackers can't attempt to force other users to use weak ciphers and eavesdrop on their communications. As of December 2024, TLS 1.3 is [supported by ca. 97% of global internet users](https://caniuse.com/tls1-3). If you need to support Internet Explorer or old versions of other browsers, you can disable TLS 1.1 and older, leaving 1.2 and 1.3 enabled. When using Ibexa Cloud, you can [set the minimum TLS version in `.platform/routes.yaml`](https://fixed.docs.upsun.com/define-routes/https.html#enforce-tls-13). ### Enable HTTP Strict Transport Security (HSTS) HSTS forces clients to always communicate with your site over HTTPS. [Most browsers support this](https://caniuse.com/stricttransportsecurity), and there is no downside for browsers that don't. Read the requirements and instructions at [hstspreload.org](https://hstspreload.org/) before you enable HSTS. Make sure to also include subdomains by means of the `includeSubDomains` setting. When using Ibexa Cloud, you can [configure HSTS in `.platform/routes.yaml`](https://fixed.docs.upsun.com/define-routes/https.html#enable-http-strict-transport-security-hsts). Beware if you are using a Varnish proxy: Your version of Varnish may not support HTTPS connections with your web server. If so, make sure to only enable HSTS between your public-facing proxy and the clients. When using Ibexa Cloud, this is handled automatically. ## Domain ### Enable Domain Name System Security Extensions (DNSSEC) DNSSEC is a DNS feature that authenticates responses to DNS requests. It protects against DNS poisoning attacks, which is when an attacker manipulates the responses to DNS requests with the goal of directing users to an IP address the attacker controls. Enabling DNSSEC involves creating the DNSSEC records in your domain, activating DNSSEC with your domain registrar, and enabling DNSSEC signature validation on all DNS servers. [Read more on DNSSEC on ICANN's website](https://www.icann.org/resources/pages/dnssec-what-is-it-why-important-2019-03-05-en). ### Enable domain update/delete protection Domain update/delete protection is a DNS setting that makes it harder for an attacker to take over a domain from the real owner, or hinder availability for users. You can enable this protection at your domain registrar's site. Log in to their site to enable these protection settings and save the new configuration. ### Enable Certificate Authority Authorization (CAA) CAA allows domain owners to specify which Certificate Authorities (CAs) are permitted to issue SSL/TLS certificates for their domain. This prevents attackers from having certificates issued for domains they don't own, hindering some types of attack. CAA is configured in your DNS zone file. ## Database ### Use UTF8MB4 with MySQL/MariaDB If you're using MySQL/MariaDB, use the UTF8MB4 database character set and related collation. The older UTF8 can lead to truncation with 4-byte characters, like some emoji, which may have unpredictable side effects. ### Secure access Secure the database access with strong passwords, keys, firewall, encryption in transit, encryption at rest, and so on, as needed. When using Ibexa Cloud, the provider handles this. ## Underlying stack To avoid exposing your application to any DDOS vulnerabilities or other yet unknown security threats, make sure that you do the following: - Avoid exposing servers on the open internet when not strictly required. - Ensure any servers, services, ports, and virtual hosts that were opened for testing purposes are shut down before going live. - Ensure file system permissions are set up in such a way that the web server or PHP user can't access files they shouldn't be able to read. Those steps aren't needed when using Ibexa Cloud, where the provider handles them. ### Track dependencies - Run servers on a recent operating system and install security patches for dependencies. - Configure servers to alert you about security updates from vendors. Pay special attention to dependencies used by your project directly, or by PHP. The provider of the operating system usually has a service for this. - Update your Composer packages regularly. Don't underestimate [package security advisories](https://doc.ibexa.co/en/saas/infrastructure_and_maintenance/security/security_advisories/#package-security-advisories) and update your dependencies so you can install the fixed versions. Also consider the risk of [supply chain attacks](https://en.wikipedia.org/wiki/Supply_chain_attack) which could be mitigated by adopting a policy of waiting a minimum amount of time before using new releases. - Enable [GitHub Dependabot](https://docs.github.com/en/code-security/concepts/supply-chain-security/dependabot-security-updates) to receive notifications when a security fix is released in a GitHub-hosted dependency. - If you're not using GitHub for your project, you can create a dummy project on GitHub with the same dependencies as your real project, and enable Dependabot notifications for that. - Ensure you get notifications about security fixes in JavaScript dependencies. ### Monitor logs - Enable logging for Cohesivo, the web server, any frontend proxies, and the database. - Monitor the logs for unusual and suspicious activity. Consider using log monitoring software to make this easier. - Consider using different accounts for manual administrative tasks and for the day-to-day running of your installation. You could for instance configure Cohesivo to use a different database user than the one you use during upgrades. This can make it easier to filter out noise in your log monitoring solution. # Reporting security issues in Ibexa products > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). Learn how to report security issues in Cohesivo. The security of Ibexa software is a primary concern and is taken seriously. For more information on security in Ibexa products, see [Ibexa Security Policy](https://www.ibexa.co/software-information/security). No engineering team is perfect though, and if you do discover a security issue in one of our products we are very grateful for your help in reporting it to us privately, and refraining from public disclosure until we have found the solution and distributed it. Thank you! ## Channels - If you're a customer or partner, please log in to your Service portal at , click "New Ticket", and report the issue as you would report a normal support request. Ibexa Product Support will respond, take care of the report, and keep you informed of the developments. - It's also possible to report security issues by email to [security@ibexa.co](mailto:security@ibexa.co) - this requires no account. ## Verbosity Please be verbose when reporting issues. The issue will be solved faster if you include: - A **title** describing the gist of the issue in one sentence - A **description** which includes the steps you take to produce the problem, what you expect the result to be, and what actually happens. - Make it clear **why you consider it a security issue**. If you know, also include its type of security issue (example: SQL injection, CSRF, Role/Policy failure), its nature (example: slowing/stopping a web site, leaking sensitive information, destroying data, privilege escalation), and how easy it is to exploit (example: Does it require editor login?). ## Dialogue The engineering team may need your help to clarify certain specifics, so please respond to such inquiries. We keep you updated about the progress on our end and may invite you as collaborators on GitHub to make communication easier. ## Responsible disclosure Please give the engineering team time to produce and distribute a solution before you disclose the issue on other channels, if you plan to do so. Please discuss the specifics with the team. ## Attribution If you want, we can include your name and/or the name of your organisation, a link, and short description about you in the security notification we send out with the fix. Thank you! # Security advisories > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). Find Ibexa security advisories, and learn how to handle Composer package security advisories. ## Ibexa security advisories Ibexa security advisories are released via [your Service portal](https://support.ibexa.co/), and via [Security advisories](https://developers.ibexa.co/security-advisories). The latter is available to non-subscribers. ## Package security advisories Overall, it's recommended to keep your Composer packages up to date. You can run the following command to check for available updates without installing them: ```bash composer update --dry-run ``` When a security issue is discovered in a Composer package, a security advisory is issued, and the affected versions of the package become blocked from installation unless you take action. When installing or updating, Composer avoids installing packages that are affected by security advisories. However, this can create constraint issues that make installation or updates impossible. For example, security fixes might not be available for [unsupported PHP versions](https://www.php.net/supported-versions.php). Example of a Composer output about a package with security issues when trying to install: ```text - Root composer.json requires twig/cssinliner-extra v3.11.0 (exact version match), found twig/cssinliner-extra[v3.11.0] but these were not loaded, because they are affected by security advisories ("PKSA-fs5b-x5k4-1h39"). ``` Composer's output doesn't always mention that a security advisory is blocking installation or updates. For example, when trying to install Cohesivo 4.6 on PHP 7.4 you might see an error like the following one: ```text - ibexa/user[v4.6.0, ..., v4.6.31] require twig/twig ^3.0 -> satisfiable by twig/twig[v3.27.0, v3.27.1, v3.28.0]. - twig/twig[v3.27.0, ..., v3.28.0] require php >=8.1.0 -> your php version (7.4.33) does not satisfy that requirement. ``` To gather more information, check package information on [Packagist](https://packagist.org/) and try installing a specific version of the package blocking installation. In this case, trying to install [`twig/twig:3.11.3`](https://packagist.org/packages/twig/twig#v3.11.3) shows explicitly that this version is blocked by a security advisory: ```text % composer require twig/twig:3.11.3 Your requirements could not be resolved to an installable set of packages. Problem 1 - Root composer.json requires twig/twig 3.11.3 (exact version match: 3.11.3 or 3.11.3.0), found twig/twig[v3.11.3] but these were not loaded, because they are affected by security advisories ("PKSA-8zx5-v2nz-58pb"). ``` It's highly recommended that you not install the affected package, and instead meet the requirements of the fixed version. You can use the [Packagist Security Advisories database](https://packagist.org/security-advisories/) to learn more about a security advisory, such as the affected packages and versions, a detailed description of the issue, and other possible reference IDs for the advisory: PKSA (Packagist Security Advisory), GHSA (GitHub Security Advisories), and CVE (Common Vulnerabilities and Exposures). If you need to, upgrade PHP and migrate your custom code to be compatible with the newer PHP version, for example by using [Rector](https://github.com/rectorphp/rector). If updating the affected package isn't possible, carefully review the security issue and assess the risk. If you choose to implement countermeasures instead of upgrading, you can ignore the security advisory. Use Composer's [`config.policy.advisories.ignore-id`](https://getcomposer.org/doc/06-config.md#ignore-id) setting, providing for each entry the reason why you consider it safe to ignore. This way, you'll still be warned if the package is affected by a new security advisory. ```json { "config": { "policy": { "advisories": { "ignore-id": { "PKSA-fs5b-x5k4-1h39": "Description of the countermeasures you've implemented causing this one to be safe to ignore.", "PKSA-8zx5-v2nz-58pb": "Description of the countermeasures you've implemented causing this one to be safe to ignore." } } } } } ``` # Support and maintenance FAQ > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). See how you can resolve common issues and report a Customer Support ticket. This page contains answers to most common questions and tips around support and maintenance, references to important parts of the documentation, and tools for developers in their daily work. ## What information should I specify when creating a Customer Support ticket? When reporting a problem to Customer Support the most important information is the version of Cohesivo which is used in the project. The best way to specify it's to provide the list of currently installed packages by running: ```bash composer show ibexa/* ``` Besides that, all the configuration from the `config` directory may be helpful. You should also list the steps to reproduce the issue, or at least provide a clear description of the circumstances under which the problem occurred. If you stumble upon a database-related problem, providing corresponding logs is also an important step. Additionally, mention recent changes, performed migrations or external scripts/code customizations related to the code which generates the problem. ## What are the recommended ways to increase my project's performance? The most important clues around increasing overall performance of your Cohesivo-based project can be found in [the Performance documentation page](https://doc.ibexa.co/en/saas/infrastructure_and_maintenance/performance/index.md). ## How can I translate my back office? The language of the back office is based on the browser language. To change it you should install the proper package for your language (see [language packages list](https://github.com/ezplatform-i18n)). Once you have language packages installed, you can switch the language of the back office in the **User Settings** menu. If you don't have a language defined in the browser, it's selected based on the `parameters.locale_fallback` parameter located in `config/packages/ibexa.yaml`. To read more about language managing in Cohesivo, see the following doc pages: - [Back office languages](https://doc.ibexa.co/en/saas/multisite/languages/back_office_translations/index.md) - [Multi-language SiteAccesses and corresponding translations](https://doc.ibexa.co/en/saas/multisite/set_up_translation_siteaccess/index.md) ## How can I apply patches to the installation? The easiest way to apply a patch to your project is by using the Unix [`patch`](https://man7.org/linux/man-pages/man1/patch.1.html) command. Remember to clear the cache afterwards. As an alternative to manually applying the patch, you can use [composer-patches](https://github.com/cweagans/composer-patches). You can apply patches received from the Support, community or the others by using your `composer.json` file. For checking the versions you're on, refer to your `composer.lock`. All you need is to specify which package receives patches and give the path/URL to the actual file. This should be done inside the `extra` section. Packages which should receive patches are removed during `composer update` or `composer require` so they can be re-installed and re-patched. When updating to the release that already contains specified patches, Composer throws an error alongside a message that they cannot be applied and are skipped ([this is configurable with 1.x](https://github.com/cweagans/composer-patches/tree/1.x#error-handling)). They can be manually removed from `composer.json` now. ## How to clear the cache properly? See [Cache clearing](https://doc.ibexa.co/en/saas/infrastructure_and_maintenance/devops/#cache-clearing) for information how to clear system, persistence, and HTTP caches. ## Where should I place my configuration files? To avoid merge conflicts on important configuration settings during upgrades, moving as much as possible of your configuration to your own files can be a good idea. All project-specific parameters should be kept in separate files. For example, configuration for Page Blocks could be placed in `config/packages/landing_page_blocks.yaml`. You can also place it in `config/landing_page_blocks.yaml`, which should be imported in `config/ibexa.yaml`: ```yaml imports: - { resource: ../landing_page_blocks.yaml } ``` ## How can I implement authentication in a Cohesivo-based project? The best approach is to use Symfony authentication. Check [development security](https://doc.ibexa.co/en/saas/infrastructure_and_maintenance/security/development_security/index.md) page for more detailed instructions. # Product guides # Product guides > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). Discover various Cohesivo features. Cohesivo product editions come with a variety of features. Discover the primary ones with the help of product guides. Condensed content allows you to quickly learn about their availability, capabilities, and benefits. - [User management product guide](https://ez-systems-developer-documentation--3381.com.readthedocs.build/en/3381/users/user_management_guide/): Find out what's user management and check what functions Cohesivo offers in this area to effectively manage the digital ecosystem. - [Content management product guide](https://ez-systems-developer-documentation--3381.com.readthedocs.build/en/3381/content_management/content_management_guide/): Read the content management product guide and learn how to create, modify, and display information to the target audience. - [Online Editor product guide](https://ez-systems-developer-documentation--3381.com.readthedocs.build/en/3381/content_management/rich_text/online_editor_guide/): Learn how to use the Online Editor, a tool that allows you to edit RichText Fields in any content item in Cohesivo. - [Page Builder product guide](https://ez-systems-developer-documentation--3381.com.readthedocs.build/en/3381/content_management/pages/page_builder_guide/): Read about the Page Builder - a powerful tool for creating and modifying pages in Cohesivo. - [Form Builder product guide](https://ez-systems-developer-documentation--3381.com.readthedocs.build/en/3381/content_management/forms/form_builder_guide/): See the Form Builder product guide and learn how to create various forms to increase the functionality of your website. - [Customer Portal](https://ez-systems-developer-documentation--3381.com.readthedocs.build/en/3381/customer_management/customer_portal/): Customer Portal allows your business clients to create and manage their company accounts. - [Product catalog guide](https://ez-systems-developer-documentation--3381.com.readthedocs.build/en/3381/product_catalog/product_catalog_guide/): The product catalog guide provides a full description of the features and capabilities for managing products, their specifications, variants, pricing, and organization. - [Quable product guide](https://ez-systems-developer-documentation--3381.com.readthedocs.build/en/3381/product_catalog/quable/quable_guide/): The Quable product guide describes how you can use the product data from Quable in Cohesivo to create marketing campaigns built around your products. - [Raptor CDP product guide](https://ez-systems-developer-documentation--3381.com.readthedocs.build/en/3381/raptor_cdp/raptor_cdp_guide/): The Raptor CDP product guide describes all the possibilities that the Customer Data Platform offers to help you build great customer experiences. - [Raptor integration product guide](https://ez-systems-developer-documentation--3381.com.readthedocs.build/en/3381/recommendations/raptor_integration/raptor_connector_guide/): Discover Raptor integration - an add-on focused on recommendations and tracking customer behaviors. - [AI Actions product guide](https://ez-systems-developer-documentation--3381.com.readthedocs.build/en/3381/ai/ai_actions/ai_actions_guide/): AI Actions help editors by automating repetitive tasks. - [MCP Servers product guide](https://ez-systems-developer-documentation--3381.com.readthedocs.build/en/3381/ai/mcp/mcp_guide/): MCP servers expose tools, specialized prompts, and resources to AI agents. # Release notes # Ibexa DXP v5.0 LTS > For the complete documentation index, see [llms.txt](https://doc.ibexa.co/en/saas/llms.txt). Ibexa DXP v5.0 incorporates features brought by LTS Updates from previous versions, brings upgrades to the tech stack and improvements to developer experience. ## TODO: Release notes for SaaS (Headless, Experience, LTS Update, New feature) Release date: 2026-07-01 ### Highlights - ASD - QWE