Custom text
{{translate 'unsubscribed' category='messages' scope='Campaign'}}
My Dashlet
` } }); ``` ## Translation Translation to dashlet is in `Global` scope, in `"dashlets"` section. __After that don't forget to Clear Cache in Administration.__ ================================================ FILE: docs/development/how-to-start.md ================================================ # How to get started *(for developers)* In this article: * [Option A. Extension development](#option-a-extension-development) * [Option B. Using git repository](#option-b-using-git-repository) * [Configuration for development](#configuration-for-development) * [Where to put customizations](#where-to-put-customizations) ## Option A. Extension development Use this approach to customize Espo for a specific business. By utilizing the [ext-template](https://github.com/espocrm/ext-template) repository, you can craft an installable extension for EspoCRM. Your repository will contain only your custom files. The ext-template tools allow you to run your extension in an Espo instance for testing purposes. See more info in the repository's readme. It is possible to [install](autoload.md) additional composer libraries in your extension. ## Option B. Using git repository Using the main EspoCRM repository. Contributors should use this approach. 1. Clone [https://github.com/espocrm/espocrm](https://github.com/espocrm/espocrm) repository (or a forked one) to your local computer. 2. Change to the project's root directory: `cd path/to/espocrm`. 3. Install [Composer](https://getcomposer.org/doc/00-intro.md) if not installed (v2.0 or greater). 4. Install npm if not installed (v8.0 or greater). 5. Install [Grunt](https://gruntjs.com/installing-grunt). 6. Run `composer install` if Composer is installed globally (or, `php composer.phar install`, if locally). 7. Run `npm ci`. Then, you can build by running `grunt`. To build a proper *config.php* file and populate database you can run installation. Open `http(s)://{YOUR_CRM_URL}/install` location in the browser. It's assumed that your webserver is properly [configured](../administration/server-configuration.md). !!! note Some dependencies require php extensions that you might not have installed. You can skip these requirements by installing with a flag *--ignore-platform-reqs*: `composer install --ignore-platform-reqs`. You also need to enable [developer mode](#configuration-for-development). After building, you will be able to run the instance in your browser right from the project root directory, considering that your web server is properly configured. ### Building 1. Change to the project's root directory. 2. Run Grunt with `grunt`. The build will be created in the `build` directory. !!! note By default, grunt installs composer dependencies. You can skip it by running `grunt offline`. #### Javascript transpiling Building with *grunt* includes the transpiling step. You can also run it manually with the following commands. Transpile all: ``` node js/transpile ``` Transpile a specific file (can be useful for a file watcher in an IDE): ``` node js/transpile -f $FilePathRelativeToProjectRoot$ ``` ### Branches * *fix* – upcoming maintenance release; fixes should be pushed to this branch; * *master* – develop branch; new features should be pushed to this branch; * *stable* – last stable release. ### Upgrade packages Preparation: 1. Fetch tags to your git repository from the remote: `git fetch --tags`. 2. Checkout to a needed version tag (or don't if you want to test upgrade to the most recent commit). 3. Build EspoCRM with grunt (see above how to build). Build the upgrade package with the command: ``` node diff {version_from} ``` The package will be created in the `build` directory. ### Using custom builds for production One may want to maintain a forked and customized repository to use it for production. Note that this is not an officially supported approach but it's still viable. Principles: 1. Merge with upstream. 2. Build upgrades by yourself. 3. Production instance should be installed from a build (artifact). It should not be the same as the development repository. 4. Do not apply upgrades against the development repository, upgrade the production instance. ## Configuration for development EspoCRM instance configuration for development. Config parameters should be set in `data/config.php`. The developer mode: ```php 'isDeveloperMode' => true, ``` !!! note The developer mode won't work on a release instance. It requires the *frontend* folder from the repository and *client/lib/transpiled* which should contain all JS files separately and transpiled. You can force using some additional cache in the developer mode. Can be reasonable as the application can run very slow w/o cache. ```php 'useCacheInDeveloperMode' => true, ``` ## Where to put customizations ### Option A. Custom dirs * `custom/Espo/Custom/` – for metadata and all files pertaining to backend * `client/custom/` – for client files ### Option B. Module dirs * `custom/Espo/Modules/{YourModuleName}/` – for metadata and all files pertaining to backend * `client/custom/modules/{your-module-name}/` – for client files This method is the only appropriate method when developing an extension. The ext-template's initialization created needed folders automatically. The important advantage of using ext-template is the ability to use ESM modules in the frontend, which significantly improves the development experience. ================================================ FILE: docs/development/index.md ================================================ # Developer Documentation ### General * [Getting started](how-to-start.md) * [Making extension package](extension-packages.md) * [Modules](modules.md) * [Tests](tests.md) * [Translation](translation.md) * [Coding rules](coding-rules.md) ### Backend * [Dependency injection](di.md) * [Metadata](metadata.md) * [ORM](orm.md) * [Select Builder](select-builder.md) * [API actions](api-action.md) * [Services](services.md) * [Hooks](hooks.md) * [ACL](acl.md) * [Entry points](entry-points.md) * Misc * [Coding practices'](coding-practices.md) * [Autoload](autoload.md) * [Entity type](custom-entity-type.md) * [Container services](container-services.md) * [Template helpers (PDF)](template-custom-helper.md) * [Formula functions](new-function-in-formula.md) * [Scheduled jobs](scheduled-job.md) * [Duplicate checking](duplicate-check.md) * [Database indexes](db-indexes.md) * [App params](app-params.md) * [Jobs](jobs.md) * [Email sending](email-sending.md) * [Calculated fields](calculated-fields.md) * [Config parameters](custom-config-parameters.md) * [Attachments](attachments.md) ### Frontend * [View](view.md) * [Model](model.md) * [Collection](collection.md) * [Templates](frontend/templates.md) * [HTML & CSS](frontend/html-css.md) * [Ajax requests](frontend/ajax.md) * [Controller & routing](frontend/controller.md) * [Dependency injection](frontend/dependency-injection.md) * [Modal dialogs](modal.md) * [Confirmation dialogs](confirm-dialog.md) * [Custom views (for records and fields)](custom-views.md) * [View setup handlers](frontend/view-setup-handlers.md) * [Save error handlers](frontend/save-error-handlers.md) * [Dynamic forms with dynamic handler](dynamic-handler.md) * Fields * [Custom field type](custom-field-type.md) * [Customizing existing fields](customize-standard-fields.md) * Misc * [Buttons & dropdown actions for detail/edit/list views](custom-buttons.md) * [Custom panels on record view](frontend/record-panels.md) * [Including custom CSS file](custom-css.md) * [Custom dashlets](how-to-create-a-dashlet.md) * [Link-multiple field with primary record](link-multiple-with-primary.md) * [Monkey patching](frontend/monkey-patching.md) * Campaigns * [Custom unsubscribe page](campaign-unsubscribe-template.md) ### API * [API Overview](api.md) ================================================ FILE: docs/development/jobs.md ================================================ # Jobs Sometimes it's reasonable to execute some actions in background. For example, when sending an email, to prevent a user to wait until sending is processed. ### Scheduling ```php create() ->setClassName($jobClassName) // should implement `Espo\Core\Job\Job` interface ->setQueue(QueueName::Q0) // optional ->setGroup('some-group-name') // optional ->setData([ 'someKey' => $someValue, ]) ->schedule(); ``` You can pass JobSchedulerFactory as a constructor dependency. ### Job ```php Need to create a `contacts` linkMultiple field with a primary for our custom entity *Stock*. > ### Step 1 Create (or edit) `custom/Espo/Custom/Resources/metadata/entityDefs/Stock.json`: ```json { "fields": { "contacts": { "type": "linkMultiple", "view": "custom:views/stock/fields/contacts" }, "contact": { "type": "link" } }, "links":{ "contact": { "type": "belongsTo", "entity": "Contact", "foreign": "stocksPrimary" }, "contacts": { "type": "hasMany", "entity": "Contact", "foreign": "stocks", "layoutRelationshipsDisabled": true } } } ``` ### Step 2 `custom/Espo/Custom/Resources/metadata/entityDefs/Contact.json` ```json { "links":{ "stocksPrimary": { "type": "hasMany", "entity": "Stock", "foreign": "contact", "layoutRelationshipsDisabled": true }, "stocks": { "type": "hasMany", "entity": "Stock", "foreign": "contacts" } } } ``` ### Step 3 `custom/Espo/Custom/Hooks/Stock/AfterSave.php` ```php isAttributeChanged('contactId')) { return; } $contactId = $entity->get('contactId'); $fetchedContactId = $entity->getFetched('contactId'); $relation = $this->entityManager ->getRDBRepository($entity->getEntityType()) ->getRelation($entity, 'contacts'); if (!$contactId) { $relation->unrelateById($fetchedContactId); return; } $relation->relateById($contactId); } } ``` ### Step 4 `client/custom/src/views/stock/fields/contacts.js` ```js define(['views/fields/link-multiple-with-primary'], (Dep) => { return class extends Dep { primaryLink = 'contact' } }); ``` ### Step 5 Run Rebuild. ### Step 6 Execute an SQL query: ```sql UPDATE stock JOIN contact_stock ON contact_stock.stock_id = stock.id AND contact_stock.deleted = 0 SET stock.contact_id = contact_stock.contact_id ``` ================================================ FILE: docs/development/metadata/acl-defs.md ================================================ # aclDefs Path: metadata > aclDefs > {ScopeName}. Defines access control parameters for a specific scope (or entity type). ## accessCheckerClassName An access checking class. Should implement `Espo\Core\Acl\AccessChecker` interface. Can optionally implement more interfaces that define what actions can be checked. Interfaces for access checking: * `Espo\Core\Acl\AccessChecker` – access to a scope; * `Espo\Core\Acl\AccessCreateChecker` – access to a create operation for a scope; * `Espo\Core\Acl\AccessReadChecker` – access to a read operation for a scope; * `Espo\Core\Acl\AccessEditChecker` – access to an edit operation for a scope; * `Espo\Core\Acl\AccessDeleteChecker` – access to a delete operation for a scope; * `Espo\Core\Acl\AccessStreamChecker` – access to the stream for a scope; * `Espo\Core\Acl\AccessEntityCreateChecker` – access to a create operation for an entity; * `Espo\Core\Acl\AccessEntityReadChecker` – access to a read operation for an entity; * `Espo\Core\Acl\AccessEntityEditChecker` – access to an edit operation for an entity; * `Espo\Core\Acl\AccessEntityDeleteChecker` – access to a delete operation for an entity; * `Espo\Core\Acl\AccessEntityStreamChecker` – access to the stream of an entity. Combined interfaces: * `Espo\Core\Acl\AccessEntityCREDChecker` – access to create/read/edit/delete of an entity (combined); * `Espo\Core\Acl\AccessEntityCREDSChecker` – access to create/read/edit/delete/stream of an entity (combined). Default class: `Espo\Core\Acl\DefaultAccessChecker`. ## ownershipCheckerClassName An ownership checking class. Should implement one of the following interfaces: * `Espo\Core\Acl\OwnershipOwnChecker` – whether a user is an owner of an entity; * `Espo\Core\Acl\OwnershipTeamChecker` – whether an entity belongs to a user team. Default class: `Espo\Core\Acl\DefaultOwnershipChecker`. ## portalAccessCheckerClassName The same as `accessCheckerClassName` but for the portal. ## portalOwnershipCheckerClassName The same as `ownershipCheckerClassName` but for the portal. Can implement additional interfaces: * `Espo\Core\Portal\Acl\OwnershipAccountChecker` * `Espo\Core\Portal\Acl\OwnershipContactChecker` ## assignmentCheckerClassName An assignment checking class. Should implement `Espo\Core\Acl\AssignmentChecker` interface. Default class: `Espo\Core\Acl\DefaultAssignmentChecker`. ## readOwnerUserField Indicates what field is used for ownership checking. If an entity uses a field other than *assignedUser* or *assignedUsers*, you need to specify that field. ## linkCheckerClassNameMap *Object.{{complexText viewObject.options.message}}
', headerText: 'Hello world', backdrop: true, message: 'Some *message*\n\nHello world!', buttonList: [ { name: 'doSomething', label: this.translate('Do Something'), onClick: () => { // Do something. this.close(); }, style: 'primary', }, { name: 'close', label: this.translate('Close'), } ], }, view => { view.render(); }); ``` ================================================ FILE: docs/development/model.md ================================================ # Model A model instance usually represents a single entity record. See the [class](https://github.com/espocrm/espocrm/blob/stable/client/src/model.js). ## Methods ### set Sets an attribute or multiple attributes. ```js // Set one attribute. model.set('attributeName', value); // Multiple at once. model.setMultiple({ attributeName1: value1, attributeName2: value2, }); // With options. model.setMultiple(attributes, { // suppresses 'change' events silent: true, }); ``` You can pass custom options and check them in 'change' event listeners. ### get Gets an attribute. ```js const value = model.get('attributeName'); // or model.attributes.attributeName; // all attributes const attributes = Espo.Utils.cloneDeep(model.attributes); ``` ### save Saves the model (to the back-end). ```js // Assuming model.id is set. try { await model.save() } catch() { // Error occurred. return; } ``` ### fetch Fetches the model (from the backend). Loads attribute values to the model. Returns a promise. ```js // Assuming model.id is set. async model.fetch(); ``` ### getClonedAttributes Get cloned attributes. Returns an object. ```js const attributes = model.getClonedAttributes(); ``` ### populateDefaults Populate default values. ```js model.populateDefaults(); ``` ### setDefs Sets field and link defs. May be needed if a model instantiated explicitly, not by the factory. ```js model.setDefs({ fields: {}, links: {}, }); ``` ## Properties ### id *string* A record ID. ### entityType *string* An entity type. ### urlRoot *string* A root API URL to use for syncing with the backend. For non-new records, an ID part will be appended. ### url *string* An API URL to use for syncing with the backend. If specified, urlRoot will be omitted. ### attributes *Record* Attribute values. ```js const name = model.attributes.name; ``` ## Instantiating Model-factory is available in views. The model-factory allows you to create a model instance of a specific entity type. ```js export default class extends View { setup() { // Use wait to hold off rendering until model is loaded. this.wait(this.loadModel()); } async loadModel() { this.model = await this.getModelFactory().create('Account'); // entityType is set by the factory. //const entityType = this.model.entityType; this.model.id = this.options.id; await model.fetch(); } } ``` Instantiating w/o factory: ```js import View from 'view'; import Model from 'model'; export default class extends View { setup() { const model = new Model(); // URL will be used when fetching and saving. model.urlRoot = 'MyModel'; model.id = 'someId'; this.wait( // This performs `GET MyModel/someId` API call. model.fetch(); ); } } ``` ## Events Note: `listenTo` and `listenToOnce` are methods of the *view* class. ### change When model attributes get changed (not necessarily synced with backend). ```js this.listenTo(model, 'change', (model, options) => { if (this.model.hasChanged('someAttribute')) { // someAttribute is changed } if (options.ui) { // changed via UI // this options is set by field view } // The 'action' option indicates how the change originated. // Possible values: fetch, save, ui, cancel-edit. console.log(options.action); // If you want to change the same model within this handler, // use setTimeout with a zero delay to avoid a potential loop. }); this.listenToOnce(model, 'change:someAttribute', (model, value, options) => { // someAttribute is changed }); ``` ### sync Model synced with backend. ```js this.listenTo(model, 'sync', (model, response, options) => { // Synced with backend – fired after fetch or save. // The 'action' option indicates how the sync originated. // Possible values: fetch, save, destroy. console.log(options.action); }); ``` ### destroy Once model is removed (after *DELETE* request). ## Additional events Defined in the application. ### after:relate Once relationship panel updated. Available on the detail view. ### after:relate:{link} Once a specific relationship panel updated. Available on the detail view. ### update-all This event is not fired. But you can fire it to refresh all relationship panels. Available on the detail view. ### update-related:{link} Fire this event to refresh a specific relationship panel. Available on the detail view. ## Other Passing model to a child view: ```js this.createView('someName', 'custom:views/some-view', { model: this.model, }); ``` Or: ```js const view = new MyView({model}); ``` ================================================ FILE: docs/development/modules.md ================================================ # Modules The best practice is to place customizations in a module directory: * `custom/Espo/Modules/{YourModule}/` – backend (CamelCase name); * `client/custom/modules/{your-module}/` – frontend (hyphen name). ## Order Every module has its *order* property. The order is used by the system to define which module to load first. For example, if two modules have some metadata with the same key, the metadata of the module that has a higher order will be used. If two modules have the controller classes with the same name, then the class of the module that has a higher order will be used. The *order* property is defined in `custom/Espo/Modules/{YourModule}/Resources/module.json`: ```json { "order": 16 } ``` Requires clearing cache after changes. ## Routes A module can define additional [API routes](api-action.md#routing). They are defined in `custom/Espo/Modules/{YourModule}/Resources/routes.json`. Requires clearing cache after changes. ## Composer You can create a `composer.json` in your module directory to include 3rd party libraries. To let Espo know about these libraries, you need to create an [autoload configuration file](autoload.md) in your module. ## JS modules When referencing ES (or AMD) modules located in an Espo module, use the path: `module/{your-module}/*`. Example: `module/my-module/views/fields/my-field`. When using [ext-template](https://github.com/espocrm/ext-template), the path to your Espo module will be automatically written in *jsconfig.json*. That will allow an IDE to properly locate module files. ================================================ FILE: docs/development/new-function-in-formula.md ================================================ # Custom functions for Formula EspoCRM provides the ability to create custom functions that can be used in formula-script. Create a file `custom/Espo/Custom/FormulaFunctions/MyContains.php` with the code: ```php 2) { $offset = $arguments[2]; return strpos($haystack, $needle, $offset) !== false; } return strpos($haystack, $needle) !== false; } } ``` Create a file `custom/Espo/Custom/Resources/metadata/app/formula.json` and add the code: ```json { "functionClassNameMap": { "myNamespace\\myContains": "Espo\\Custom\\FormulaFunctions\\MyContains" }, "functionList": [ "__APPEND__", { "name": "myNamespace\\myContains", "insertText": "myNamespace\\myContains(HAYSTACK, NEEDLE, OFFSET)" } ] } ``` Clear cache. ================================================ FILE: docs/development/orm-value-objects.md ================================================ # Value Objects *As of v7.0.* * Value objects are immutable. * Value objects contain modification methods that return a cloned instance. * Default value objects are available in the namespace `Espo\Core\Field`. * It's possible to register custom value object for a specific field type or for a specific field. When defining getters and setters in an Entity class, it's recommended to use value objects for such field types: * date * datetime * datetimeOptional * address * currency All field types with registered value object: * date * datetime * datetimeOptional * address * currency * email * phone * link * linkParent * linkMultiple BaseEntity's methods *getValueObject* and *setValueObject* will work for field types with registered value object. ```php getValueObject($field); $entity->setValueObject($field, $valueObject); $entity->setValueObject($field, null); ``` Getter and setter: ```php get('dateDue'); if ($rawValue === null) { return null; } return Date::fromString($rawValue); } public function setDateDue(?Date $dateDue): self { $this->setValueObject('dateDue', $dateDue); return $this; } } ``` ## Supported field types ### Address ```php getBillingAddress() ?? new Address(); $country = $address->getCountry(); $city = $address->getCity(); ``` ```php withCity($city) ->withCountry($country) ->withPostalCode($postalCode); $accountEntity->setBillingAddress($address); ``` ### Currency ```php convert($value, 'EUR'); $opportunityEntity->setAmount($valueInEur); ``` ### Email address, Phone number ```php getEmailAddressGroup(); $primary = $emailAddressGroup->getPrimary(); $modifiedEmailAddressGroup = $emailAddressGroup ->withAddedEmailAddress( EmailAddress::create('address@test.com')->optedOut() ); $accountEntity->setEmailAddressGroup($modifiedEmailAddressGroup); ``` The same is available for phone numbers. ### Date, DateTime, DateTimeOptional ```php getCloseDate(); $opportunityEntity->setCloseDate( $closeDate->modify('+1 month') ); ``` ### Link, Link-Parent, Link-Multiple ```php getId()) ->withColumnValue('role', 'Decision Maker'); $contacts = LinkMultiple::create([$contact->getId()]); ``` ## Registering !!! note Registering own value object types might be excessive as it's possible to manually handle value object creation and consumption in getter and setters of an entity. Registering a custom value object type for a specific field type. For a field type you need to define 2 parameters in metadata > fields > {fieldType}: * `valueFactoryClassName` – implementation of `Espo\ORM\Value\ValueFactory` interface; * `attributeExtractorClassName` – implementation of `Espo\ORM\Value\AttributeExtractor` interface. It's also possible to register a value object for a specific field in metadata > entityDefs > {entityType} > fields > {fieldName}: * `valueFactoryClassName`; * `attributeExtractorClassName`. ================================================ FILE: docs/development/orm.md ================================================ # ORM EspoCRM utilizes own built-in ORM (object-relational mapping). Create, update, read, delete and search operations are performed via the Entity Manager instance. The *EntityManager* is available as a [*container service*](di.md). It's a central access point for ORM functionalities. A *Repository* class serves for fetching and storing records. Base classes: `Espo\ORM\Repositories\RDBRepository`, `Espo\Core\Repositories\Database`. *RDB* stands for a *relational database*. An *Entity* class represents a single record. Each [entity type](../administration/terms-and-naming.md#entity-type) has its own entity class. Base class: `Espo\Core\ORM\Entity`, interface: `Espo\ORM\Entity`. An *EntityCollection* is a collection of entities. It's returned by *find* operations. An *SthCollection* is a collection of entities, consuming much less memory than EntityCollection. Collections are iterable. The ORM uses a data-mapper approach. Note that it does not have an identity map – fetching the same record multiple times in a row will return different instances (of the same record). ## See also * [Complex expressions](../user-guide/complex-expressions.md) * [Value objects](orm-value-objects.md) * [Custom entity type](custom-entity-type.md) * [Entity definitions](metadata/entity-defs.md) ## Injecting Entity Manager The Entity Manager is available as a [*Container*](di.md) service. A class with the `entityManager` dependency: ```php getNewEntity($entityType); ``` Or type hinted: ```php getRDBRepositoryByClass(MyEntity::class)->getNew(); ``` !!! note It creates a new instance but doesn't store it in DB. The entity doesn't have an ID yet. ### Fetch existing entity ```php getEntityById($entityType, $id); ``` Or type hinted: ```php getRDBRepositoryByClass(MyEntity::class)->getById($id); ``` ### Store entity ```php saveEntity($entity); ``` #### Save options Save with options: ```php saveEntity($entity, [SaveOption::SILENT => true]); ``` Available options: * skipAll – skip all additional processing; * skipHooks – skip all hooks; workflows, formula will be ignored; * silent – workflows will be ignored, modifiedAt and modifiedBy fields won't be changed; * skipCreatedBy – createdBy won't be set to the current user; * skipModifiedBy – modifiedBy won't be set to the current user; * createdById – override createdBy with a passed user ID; * modifiedById – override modifiedBy. Options in constants available here: `Espo\Core\ORM\Repository\Option\SaveOption`. ### Create and store entity Stores the record in DB and returns an entity instance. ```php createEntity($entityType, [ 'name' => 'Test', 'status' => 'Hello', ]); ``` ### Remove entity Soft-deletes the record. ```php removeEntity($entity); ``` ### Get attribute value Return the value of a given attribute. An attribute usually corresponds to a column in DB. ```php get('attributeName'); ``` !!! note As EspoCRM supports custom fields and relationships which are added dynamically without the need to compile, attribute accessor methods *get*, *set* and *has* were introduced. For type safety, consider creating getters and setters for needed attributes in your custom Entity class. Use these methods in your business logic code. ### Has attribute value Checks whether an attribute is set. Note: If it's set to *null*, it will return *true*. An attribute may not be set if the entity was fetched with only a specified attribute list. ```php has('attributeName'); // true or false ``` ### Set attribute value One: ```php set('attributeName', 'Test Value'); ``` Multiple: ```php setMultiple([ 'name' => 'Test Name', 'assignedUserId' => '1', ]); ``` ### Clear attribute value This will unset the attribute. If you save the Entity after that, it will not change the value to NULL in database. Very rarely used. Not recommended. ```php clear('attributeName'); ``` ### Fetched attributes Check whether an attribute was changed. ```php getFetched('attributeName'); // check whether an attribute was changed since the last syncing with DB $attributeChanged = $entity->isAttributeChanged('attributeName'); ``` ### Get all attribute values Returns all attributes with their values. ```php getValueMap(); ``` ### Delete from DB Deletes the record permanently. ```php getRDBRepository($entityType)->deleteFromDb($id); ``` ## Repository Use ORM's repositories to fetch and save entities. A repository instance is instantiated per entity type. !!! note It may be reasonable to wrap all interactions with the repository in a higher-level class (usually also called a Repository), so that your business-logic classes do not depend directly on the Entity Manager. This improves testability. Get a repository by an entity class: ```php `. $accountRepository = $entityManager->getRDBRepositoryByClass(Account::class); ``` ```php getRDBRepositoryByClass(Account::class) ->getById($id); ``` Note: To be able to fetch the repository by an entity type, your [custom entity](custom-entity-type.md#entity-class) needs to have the *ENTITY_TYPE* constant. Get a repository by an entity type: ```php getRDBRepository($entityType); ``` ### Find ```php getRDBRepository($entityType) ->where([ 'type' => 'Customer', ]) ->find(); ``` Descending order: ```php getRDBRepository($entityType) ->limit(0, 10) ->order('createdAt', 'DESC') ->find(); ``` Complex order: ```php getRDBRepository($entityType) ->order([ ['createdAt', 'ASC'], ['name', 'DESC'], ]) ->find(); ``` Or: ```php getRDBRepository($entityType) ->order('createdAt', 'ASC') ->order('name', 'DESC') ->find(); ``` Or: ```php getRDBRepository($entityType) ->order( Expr::concat( Expr::column('firstName'), Expr::column('lastName') ), 'DESC', ) ->find(); ``` Ordering by a value list: ```php getRDBRepository('Opportunity') ->order('LIST:stage:Prospecting,Qualification,Proposal') ->find(); ``` Feeding a query to a repository: ```php getRDBRepository($entityType) ->clone($query) ->limit(0, 10) ->find(); ``` Finding the first one: ```php getRDBRepository($entityType) ->where([ 'type' => 'Customer', ]) ->findOne(); ``` You can use *getRDBRepositoryByClass* for type safety: ```php getRDBRepositoryByClass(Account::class) ->findOne(); // Static analysis infers the entity's type. ``` ### Get new Prepare a new entity without saving it: ```php getRDBRepositoryByClass(Account::class) ->getNew(); ``` ### Save Save an entity: ```php getRDBRepositoryByClass(Account::class) ->save($account); ``` ### Remove Remove an entity (soft delete): ```php getRDBRepositoryByClass(Account::class) ->remove($account); ``` ## Relations !!! note As of v8.4, the *getRelation* method is available in the *EntityManager*. `$entityManager->getRelation($entity, 'relationName');` Before, it could be accessed from a repository. ### Find related Has-Many: ```php getRelation($account, 'opportunities') ->find(); // Filter. $opportunityCollection = $entityManager ->getRelation($account, 'opportunities') ->limit(0, 10) ->where($whereClause) ->find(); // First one. $opportunity = $entityManager ->getRelation($account, 'opportunities') ->order('createdAt', 'DESC') ->findOne(); ``` Belongs-To or Has-One: ```php getRelation($task, 'account') ->findOne(); ``` Filtering by a relation column: ```php getRelation($targetList, 'leads') ->where(['@relation.optedOut' => false]) ->find(); ``` *optedOut* is a column in the middle table. ### Relate entities ```php getRelation($account, 'opportunities') ->relate($opportunity); $entityManager ->getRelation($account, 'opportunities') ->relateById($opportunityId); // With relationship column setting. $entityManager ->getRelation($account, 'contacts') ->relate($contact, ['role' => 'CEO']); ``` ### Unrelate entities ```php getRelation($account, 'opportunities') ->unrelate($opportunity); $entityManager ->getRelation($account, 'opportunities') ->unrelateById($opportunityId); ``` ### Update columns ```php getRelation($account, 'contacts') ->updateColumns($contact, ['role' => 'CEO']); $entityManager ->getRelation($account, 'contacts') ->updateColumnsById($contactId, [ 'role' => 'CEO', // relationship column ]); ``` ### Check whether related ```php getRelation($account, 'opportunities') ->isRelated($opportunity); ``` ### Managing from within Entity class *As of v9.0.* A developer can add getter and setters to an Entity class which will allow to read and set related records. In a custom entity type class, you can define getter and setters for relationships. If you call the same getter multiple times, it will return the same instance. It's useful when the same related entity is accessed in multiple hooks during save. After save, the map will be reset – the getter won't return the same instance as before save. Get one: ```php relations->getOne('account'); } ``` Set one: ```php setRelatedLinkOrEntity('account', $account); } ``` Get many: ```php */ public function getAccounts(): Traversable { /** @var Traversable{{viewObject.someParam1}}
{{someParam2}}
` // Alternatively, a template can be defined in a separate file. // The `custom` prefix indicates that the base path is `client/custom/res/templates`. //template = 'custom:test/my-custom-view' // Initializing. Called on view creation, the view is not yet rendered. setup() { // Calling the parent `setup` method, can be omitted. super.setup(); // Instantiate some property. this.someParam1 = 'test 1'; this.addHandler('focus', '.record input[data-name="hello"]', (event, target) => { // Do something. }); // When we create a child view in the setup method, rendering of the view is held off // until the child view is loaded (ready), the child view will be rendered along with the parent view. // The first argument is a key name that can be used to access the view further. // The second argument is a view name. // The method returns a promise that resolves to a view object. this.createView('someKeyName', 'custom:test/my-custom-child-view', { // A relative selector of the DOM container. selector: '.some-test-container', // Or a full selector. //fullSelector: '#some-id', // Pass some parameter. someParam: 'test', }); // Options passed from the parent view. console.log(this.options); // A model can be passed from the parent view. console.log(this.model); // All event listeners are recommended to be initialized in the `setup` method. // Use listenTo & listenToOnce methods for listening to events of another object // to prevent memory leakage. // Subscribe to model change. // Subscribing with the `listenTo` method guarantees automatic unsubscribing on view removal, // so there won't be a memory leak. this.listenTo(this.model, 'change', () => { // Whether a specific attribute changed. if (this.model.hasChanged('someAttribute')) { const value = this.model.get('someAttribute'); } }); // Subscribe to model sync (saved or fetched). Fired only once. this.listenToOnce(this.model, 'sync', () => {}); // Subscribe to a DOM event. `cid` contains an ID unique among all views. // Requires explicit unsubscribing on view removal. $(window).on('some-event.' + this.cid, () => {}); // Translating a label. const translatedLabel = this.translate('myLabel', 'someCategory', 'MyScope'); } // Called after contents is added to the DOM. afterRender() { // The view container (DOM element). console.log(this.element); // Accessing a child view. const childView = this.getView('someKeyName'); // Checking whether a view is set. const hasSomeView = this.hasView('someKeyName'); // Destroying a child view, also removes it from DOM. this.clearView('someKeyName'); // Initializing a reference to some DOM element. this.$someElement = this.$el.find('.some-element'); } // Data to be passed to the template. data() { return { someParam2: 'test 2', }; } // Called when the view is removed. // Useful for destroying event listeners initialized for the view. onRemove() { $(window).off('some-event.' + this.cid); } // A custom method. someMethod1(value) { // Create and render a child view. this.createView('testKey', 'custom:test/my-another-custom-child-view', { selector: '.another-test-container', value: value, }) .then(view => view.render()); } someMethod2() { // To proceed only when the view is rendered. // Useful when the method can be invoked by a caller before the view is rendered. this.whenRendered().then(() => { // Do something with DOM. }); } } } ``` See more about [templates](frontend/templates.md). See [the source file](https://github.com/yurikuzn/bull/blob/master/src/bull.view.js) of the view class. ## Waiting for some data loaded before rendering Sometimes we need to get some data loaded asynchronously before the view is rendered. For this purpose we can use the `wait` method inside the `setup` method. The `wait` method can receive a promise: ```js setup() { this.wait( Promise.all([ this.model.fetch(), this.model.collection.fetch(), ]) ); } ``` The model factory returns a promise, so we can pass it to the `view` method: ```js setup() { this.wait( this.getModelFactory().create('Case') .then(model => { model.id = this.model.id; return model.fetch(); }) .then(data => { console.log(data); }) ); } ``` Wait until a model is fetched. The `fetch` method returns a promise. ```js setup() { this.wait( this.model.fetch() ); } ``` Wait for multiple independent promises: ```js setup() { this.wait( this.model.fetch() ); this.wait( Espo.Ajax.getRequest('SomeUrl') ); } ``` ```js setup() { this.wait( Promise.all([ this.model.fetch(), Espo.Ajax.getRequest('SomeUrl'), ]) ); } ``` A simple way to wait: ```js setup() { // This holds off the rendering. this.wait(true); Espo.Ajax.getRequest('Some/Request') .then(response => { // This cancels waiting and proceeds to rendering. this.wait(false); }); } ``` !!! note Feel free to use the async/await syntax instead of explicit promises. ### setup Called internally on initialization. Put initialization logic here. Options passed by the parent view are available in `this.options`. ### afterRender Called internally after render. Put manipulation with DOM here. ### onRemove Called internally on view removal. Reasonable for unsubscribing. ### createView Creates a child view. When we create a child view in the setup method, rendering of the view is held off until the child view is loaded (ready), the child view will be rendered along with the parent view. The first argument is a key name that can be used to access the view further (with `getView` method). The second argument is a view name. The method returns a promise that resolves to a view object. Arguments: * *viewKey* – a view key; * *viewName* – a view name (path); * *options* – options. Standard options (all are optional): * *selector* – a relative CSS to the view selector (as of v7.3); * *model* – a model; * *collection* – a collection. It's important that every view have their actual selector so that the application knows how to access them (for re-rendering). ### clearView Removes a child view. Arguments: * *viewKey* – a view key. ### getView Get a child view by a key. Arguments: * *viewKey* – a view key. ### assignView *As of v7.5.* Assign a view instance as a child view. Arguments: * *viewKey* – a view key; * *view* – a view instance; * *selector* – a relative CSS selector. ```js this.assignView('someKey', new SomeView(options), 'some-selector'); ``` ### reRender Re-renders a view. Usually, called from inside the view. Returns a promise resolved once rendering is finished. Arguments: * *force* – *boolean* – force rendering if the view was not rendered before. ### render Renders a view. Should be called if the view is called not in the *setup* method (after the view is already ready or rendered). Returns a promise resolved once rendering is finished. ```js templateContent = `
{{#each paymentRequests}}
{{#if (equal status 'Pending') }}
Payment Link {{number}}
{{/if}}
{{/each}}
| {{name}} | {{assignedUserName}} |
{{lookup ../teamsNames this}}
``` ### Expressions * `{{object.key}}` – lookup in objects; * `{{array.[0]}}` – lookup in arrays; * `{{~anyTag}}` – remove all previous spacing; * `{{anyTag~}}` – remove all next spacing; * `{{{helper}}}` – prevent escaping; * `{{helper1 (helper2 arg1 arg2) arg2 arg3}}` – sub-expression passed as an argument; ### Logical expressions *As of 8.4.* Helpers: * and * or * not To be used in sub-expressions. Examples: ``` {{#if (or (equal status 'Draft') (equal status 'Active'))}} ... {{/if}} ``` ``` {{#if (and value1 value2 value3)}} ... {{/if}} ``` ### Images ``` {{imageTag imageFieldNameId width=50 height=50}} ``` * *imageFieldNameId* is a name of image field, concatenated with *Id* * *width* and *height* can be omitted Another legacy way to print images: ```