Skip to main content
page last edited on 18 July 2022

Updating add-ons from 5.4 to 5.5 branch

Version: 5.5

This is a guide that will help you to adapt your add-on to X-Cart 5.5 version. Please be sure to apply those changes only on dev copy of your store.

Installation Process

X-Cart 5.5.0.x no longer has an installation wizard. The installation is done by configuring the store's ENV variables in .env.local and running an installation script in the console.

  1. Download the X-Cart 5.5.0.x distribution package.

  2. Extract the package on the server.

    tar -xzpf x-cart-5.5.0.0-platform-en.tar.gz
  3. Copy the default file containing the environment variables to a local file and make the necessary changes.

    cp .env .env.local

    You need to set correct values for the following settings:

  • DATABASE_URL - actual database access info (without the mysql prefix).
  • DATABASE_DEFAULT_TABLE_PREFIX - a correct prefix for the tables without the trailing underscore.
  • XCART_HOST_DETAILS_HTTP_HOST and XCART_HOST_DETAILS_HTTPS_HOST - actual http and https hosts of your store.
  • XCART_HOST_DETAILS_WEB_DIR - if XC resides in a subfolder of the server root, specify the path starting with a slash.
  1. Run the installation script install.sh with the -a parameter to create a root admin and set a password for them.
    ./bin/install.sh -aadmin@example.com:securepassword
  2. Make sure the web server is configured properly for use with X-Cart. Some examples of nginx and apache configurations can be found in the docs folder of the distribution package.

Add-on Folder Structure

Changes of add-on folder structure:

  • For the purpose of separation of the core code and the code of add-ons, all the add-ons have been moved from /classes/XLite/Module to /modules
  • yaml files (main.yaml, install.yaml) have been moved from the add-on root to /modules/[Author]/[Name]/config
  • Add-ons icons have been moved from the add-on root to /modules/[Author]/[Name]/config/images Skin previews have been moved from /skins/admin/modules/[Author]/[Name]/preview* to the folder /modules/[Author]/[Name]/config/images
  • All the front end resources (js, css, images) have been moved from /skins/* to /modules/[Author]/[Name]/public/ . Note that it is required to follow the folder structure convention (The admin, customer and common folders have been moved to the web subfolder for the purpose of explicit separation into interfaces):
/modules/[Author]/[Name]/public/
mail # interface
admin # area
modules/[Author]/[Name]/...
common
modules/[Author]/[Name]/...
customer
modules/[Author]/[Name]/...
pdf
admin
modules/[Author]/[Name]/...
common
modules/[Author]/[Name]/...
customer
modules/[Author]/[Name]/...
web
admin
modules/[Author]/[Name]/...
common
modules/[Author]/[Name]/...
customer
modules/[Author]/[Name]/...

Skins can override the resources of the core and of the other add-ons by using an appropriate path within the interface and the area.

  • All the templates have been moved from /skins/* to /modules/[Author]/[Name]/templates/ Note that it is required to follow the folder structure convention (The admin, customer and common folders have been moved to the web subfolder for the purpose of explicit separation into interfaces):
/modules/[Author]/[Name]/public/
mail # interface
admin # area
modules/[Author]/[Name]/...
common
modules/[Author]/[Name]/...
customer
modules/[Author]/[Name]/...
pdf
admin
modules/[Author]/[Name]/...
common
modules/[Author]/[Name]/...
customer
modules/[Author]/[Name]/...
web
admin
modules/[Author]/[Name]/...
common
modules/[Author]/[Name]/...
customer
modules/[Author]/[Name]/...

Skins can override the templates of the core and of the other add-ons by using an appropriate path within the interface and the area.

  • It is possible to specify a list of files for autoloading in main.yaml
autoloader:
- vendor/autoload.php

It is advisable to add dependencies via the composer by creating composer.json at the root level and loading libraries into the /modules/[Author]/[Name]/vendor/ subfolder. In the cases when dependencies cannot be added via the composer, they should be placed in the folder /modules/[Author]/[Name]/lib/. In the same folder, you also need to create autoload.php, which should be organised in the same way as the composer, which is to say that X-Cart does only an include of this file.

  • All the rest of the code needs to be placed in the folder /modules/[Author]/[Name]/src/
  • Tests should be created in /modules/[Author]/[Name]/tests/
  • We have removed the prefix XLite\Module from the namespace of module classes. The namespace prefix for test classes is [Author]\[Name]\Tests\*
  • To define a add-on as a Symfony bundle, it is necessary to create a class /modules/[Author]/[Name]/src/[Name]Bundle.php Note that the configuration file /modules/[Author]/[Name]/config/services.yaml will be loaded automatically. It is required because X-Cart add-ons bring not just tools like Symfony bundle, but also the final functionality.

Templates

  • Twig has been updated to version 3.3.7 There has been a change to some features; for example, instead of
{% spaceless %}
...
{% endspaceless%}

you now need to use

{% apply spaceless %}
...
{% endapply %}
  • All the core templates have been moved from /skins/* to /templates/ All the add-on templates have been moved from /skins/* to /modules/[Author]/[Name]/templates/ Note that it is required to follow the folder structure convention.
/modules/[Author]/[Name]/public/
mail # interface
admin # area
modules/[Author]/[Name]/...
common
modules/[Author]/[Name]/...
customer
modules/[Author]/[Name]/...
pdf
admin
modules/[Author]/[Name]/...
common
modules/[Author]/[Name]/...
customer
modules/[Author]/[Name]/...
web
admin
modules/[Author]/[Name]/...
common
modules/[Author]/[Name]/...
customer
modules/[Author]/[Name]/...

Skins can override the templates of the core and of the other add-ons by using an appropriate path within the interface and the area. For example:

# Core template
/templates/web/customer/layout/content/center.twig
# Add-on template that overrides the core template
/modules/[Author]/[Name]/templates/web/customer/layout/content/center.twig
# Add-on's own template
/modules/[Author]/[Name]/templates/web/customer/modules/[Author]/[Name]/settings.twig
  • The compiled template code does not bind into the View/* class, which means that private and protected methods from the class cannot be called in the template. The class itself is still accessible by the name this.
  • Part of the “magic” has been removed, which means that when calling a method in the templates you need to specify the method's full name. {this.product} → {this.getProduct()} When accessing a field you need to use the get method. {this.page} → {this.get('page')}

Decorator (meta-decorator, extender)

The general concept and principles have not changed.

  • Instead of the interface \XLite\Base\IDecorator you need to specify the Mixin annotation. It is advisable that the class be abstract, but a strict check has not been implemented yet.
use XCart\Extender\Mapping\Extender;

/**
* @Extender\Mixin
*/
abstract class Cart extends \XLite\Controller\Customer\Cart
  • Support for @Decorator\* and LC_Dependencies annotations has been removed completely. Now you need to use @Extender\* annotations, and they need to be imported. They work the same way as @Decorator\*.
use XCart\Extender\Mapping\Extender;

/**
* @Extender\Mixin
* @Extender\After()
* @Extender\Before()
* @Extender\Depend()
* @Extender\Rely()
*/
abstract class Cart extends \XLite\Controller\Customer\Cart
  • Locked annotation has been added to forbid class extension.
use XCart\Extender\Mapping\Extender;

/**
* @Extender\Locked
*/
class SomeClass
  • The principle of autoloading has changed, thanks to which, both in prod and in dev mode, the existence of a class can always be checked using a built-in php method (class_exists, interface_exists, trait_exists).

Database

There have been some changes as to how annotations are used. Instead of short names you now need to use imports. Due to technical limitations and for better performance, they need to be done in a special way: Doctrine\ORM\Mapping needs to be imported with an ORM alias and needs to be used appropriately.

Used to be:

/**
* The "profile" model class
*
* @Entity
* @Table (name="profiles",
* indexes={
* @Index (name="cms_profile", columns={"cms_name","cms_profile_id"}),
* @Index (name="login", columns={"login"}),
* @Index (name="order_id", columns={"order_id"}),
* @Index (name="password", columns={"password"}),
* @Index (name="access_level", columns={"access_level"}),
* @Index (name="first_login", columns={"first_login"}),
* @Index (name="last_login", columns={"last_login"}),
* @Index (name="status", columns={"status"})
* }
* )
* @HasLifecycleCallbacks
*/
class Profile extends \XLite\Model\AEntity

Now:

use Doctrine\ORM\Mapping as ORM;

/**
* The "profile" model class
*
* @ORM\Entity
* @ORM\Table (name="profiles",
* indexes={
* @ORM\Index (name="login", columns={"login"}),
* @ORM\Index (name="order_id", columns={"order_id"}),
* @ORM\Index (name="password", columns={"password"}),
* @ORM\Index (name="access_level", columns={"access_level"}),
* @ORM\Index (name="first_login", columns={"first_login"}),
* @ORM\Index (name="last_login", columns={"last_login"}),
* @ORM\Index (name="status", columns={"status"})
* }
* )
* @ORM\HasLifecycleCallbacks
*/
class Profile extends \XLite\Model\AEntity
  • The loading of fixtures has not been changed, with the exclusion of the install.yaml file location: this file needs to be placed in the /modules/[Author]/[Name]/config/ folder.
  • The connection with the model for the translation of fields needs to be described explicitly.
namespace XLite\Model;

/**
* @ORM\Entity
* @ORM\Table (name="some_model")
*/
class SomeModel extends \XLite\Model\Base\I18n
{
/**
* @var \Doctrine\Common\Collections\Collection
*
* @ORM\OneToMany (targetEntity="XLite\Model\SomeModelTranslation", mappedBy="owner", cascade={"all"})
*/
protected $translations;

public function getSomeField(): string
{
return $this->getTranslationField(__FUNCTION__);
}

public function setCountry(string $someField): void
{
$this->setTranslationField(__FUNCTION__, $someField);
}
}
namespace XLite\Model;

/**
* @ORM\Entity
* @ORM\Table (name="some_model_translations",
* indexes={
* @ORM\Index (name="ci", columns={"code","id"}),
* @ORM\Index (name="id", columns={"id"})
* }
* )
*/
class SomeModelTranslation extends \XLite\Model\Base\Translation
{
/**
* @ORM\Column (type="string", length=64)
*/
protected string $someField;

/**
* @var \XLite\Model\SomeModel
*
* @ORM\ManyToOne (targetEntity="XLite\Model\SomeModel", inversedBy="translations")
* @ORM\JoinColumn (name="id", referencedColumnName="code", onDelete="CASCADE")
*/
protected $owner;

public function setCountry(string $someField): void
{
$this->someField = $someField;
}

public function getCountry(): string
{
return $this->someField;
}
}

ViewLists

There have not been any changes about templates; the ListChild annotation is used based on the short name.

{##
# Column with checkboxes
#
# @ListChild (list="itemsList.product.modify.common.admin.columns", weight="10")
#}

In classes, an annotation needs to be imported.

use XCart\Extender\Mapping\ListChild;

/**
* Category products list widget
*
* @ListChild (list="center.bottom", zone="customer", weight="200")
*/
class Main extends \XLite\View\ItemsList\Product\Customer\Category\ACategory

A new parameter, interface, has been added; the possible values for this parameter are web (default), mail, andpdf.

To change the set of lists in skins, you need to mark the add-on as a symfony bundle and to process the following events:

  • xcart.service.view-list.collect-mutation.before
  • xcart.service.view-list.collect-mutation
  • xcart.service.view-list.collect-mutation.after
  • xcart.service.view-list.apply-mutation.before
  • xcart.service.view-list.apply-mutation.after

In the event handler we get the object \XCart\Event\Service\ViewListMutationEvent whose methods are used to make changes to lists.

public function addMutations(array $mutations): void
{
foreach ($mutations as $subject => $mutation) {
$this->addMutation($subject, $mutation);
}
}

/**
* The mutation can be in the following formats:
* 1. To remove from single list:
* [
* {remove_definition}
* ]
* 2. To remove from single list and insert to single list (move)
* [
* {remove_definition},
* {insert_definition}
* ]
* 3. To remove from several lists or/and insert to several lists
* [
* 'to_remove' => [{remove_definition}, ...],
* 'to_insert' => [{insert_definition}, ...]
* ]
* {remove_definition}: 'list_name' | ['list_name'{, 'interface'}]
* {insert_definition}: 'list_name' | ['list_name'{, (int) weight{, 'interface'}}]
*
* @param string $subject FQCN or Template relative path
* @param array $mutation Mutation definition
*/
public function addMutation(string $subject, array $mutation): void

Example

Handler registration (/modules/[Author]/[Name]/config/services.yaml)

services:
[Author]\[Name]\Core\EventListener:
tags:
- { name: kernel.event_listener, event: xcart.service.view-list.collect-mutation, method: onCollectViewListMutations }

Handler

namespace [Author]\[Name]\Core;

use XLite;
use XCart\Event\Service\ViewListMutationEvent;

final class EventListener
{
public function onCollectViewListMutations(ViewListMutationEvent $event): void
{
$event->addMutations([
'layout/content/main.location.twig' => [
ViewListMutationEvent::TO_REMOVE => [
['layout.main', XLite::INTERFACE_WEB, XLite::ZONE_CUSTOMER ],
],
ViewListMutationEvent::TO_INSERT => [
['center.top', 1000, XLite::INTERFACE_WEB, XLite::ZONE_CUSTOMER ],
],
]
])
}
}

To check for enabled add-ons, the service XCart\Domain\ModuleManagerDomain is used.

services:
[Author]\[Name]\Core\EventListener:
arguments:
$moduleManagerDomain: '@XCart\Domain\ModuleManagerDomain'
tags:
- { name: kernel.event_listener, event: xcart.service.view-list.collect-mutation, method: onCollectViewListMutations }
namespace [Author]\[Name]\Core;

use XCart\Domain\ModuleManagerDomain;
use XCart\Event\Service\ViewListMutationEvent;
use XLite;

final class EventListener
{
private ModuleManagerDomain $moduleManagerDomain;

public function __construct(
ModuleManagerDomain $moduleManagerDomain
) {
$this->moduleManagerDomain = $moduleManagerDomain;
}

if ($this->moduleManagerDomain->isEnabled('CDev-GoSocial')) {
$event->addMutations([
'modules/CDev/GoSocial/product/details/parts/common.share.twig' => [
ViewListMutationEvent::TO_REMOVE => [
['product.details.page.info', XLite::INTERFACE_WEB, XLite::ZONE_CUSTOMER ],
],
ViewListMutationEvent::TO_INSERT => [
['product.details.page.image', 20, XLite::INTERFACE_WEB, XLite::ZONE_CUSTOMER ],
],
],
]);
}
}

Configuration

The dynamic configuration has not been changed. Add-ons declare variables in modules/[Author]/[Name]/config/install.yaml

XLite\Model\Config:
- name: some_variable
category: [Author]\[Name]
type: hidden
value: ''
orderby: 100
translations:
- code: en
option_name: Some Variable

Access is provided via a singleton:

\XLite\Core\Config::getInstance()->[Author]->[Name]->some_variable

Static Configuration

  • The folder etc/ has been removed.
  • Configuration is stored in the file /config/packages/x_cart.yaml
  • The general principle from Symfony is used: Configuration

Logging

Logging is done via the https://github.com/Seldaek/monolog library.

Core

The core setup is done via Symfony https://symfony.com/doc/current/logging.html

The core uses 2 constructors:

  • \Includes\ErrorHandler - serves mostly the back-compatibility purposes.
  • \XLite\Logger - the main constructor, though not used directly.

Both constructors are wrappers to get a service from a DI container.

Using \Includes\ErrorHandler

use Includes\ErrorHandler;
use Psr\Log\LoggerInterface;

/** @var LoggerInterface $logger */
$logger = ErrorHandler::getLogger('xlite');

$logger->info('Some message', ['additionalData' => 'value']);
public static function getLogger(string $name): LoggerInterface;
  • $name - the logger name, do not use as a file name - {root}/var/log/{year}/{month}/xlite.log{year}-{month}-{day}.php

For messages at the Monolog\Logger::DEBUG level and higher, backtrace is added automatically. The trace field from the message is parsed concurrently. That is required to log exceptions:

use Includes\ErrorHandler;
use Psr\Log\LoggerInterface;

/** @var LoggerInterface $logger */
$logger = ErrorHandler::getLogger('xlite');

try {
...
} catch (\Exception $e) {
$logger->error($e->getMessage(), ['trace' => $e->getTrace()]);
}

Using \XLite\Logger.

Must be used with trait \XLite\InjectLoggerTrait:

use \XLite\InjectLoggerTrait;

class SomeClass
{
use InjectLoggerTrait;

public static function staticAction()
{
$logger = static::getLogger();

$logger->info('Some message', ['additionalData' => 'value']);
}

public function action()
{
$logger = $this->getLogger();

$logger->info('Some message', ['additionalData' => 'value']);
}

public function anotherAction()
{
$this->logPostponed(LOG_DEBUG, 'Some message', ['additionalData' => 'value']);
}
}
protected static function getStaticLogger(string $name = 'xlite'): LoggerInterface;
protected function getLogger(string $name = 'xlite'): LoggerInterface;
protected function logPostponed(int $level = 0, string $message = '', $context = []): void;
  • Use the xlite name for messages from the core, and {author}-{name} name for add-ons (e.g.: CDev-Paypal). The name is added to the file records.

Using custom files for logging

To use a custom file, you must specify your logger channel modules/[Author]/[Name]/config/services.yaml

monolog:
channels: [ custom ]
handlers:
custom:
level: debug
type: stream
path: '%kernel.logs_dir%/custom.log'
channels: [ custom ]

Then extract the logger from the container,

$container = \XCart\Container::getContainer();
$logger = $container ? $container->get('monolog.logger.custom') : new \Psr\Log\NullLogger();
$logger->error('Message');

or get auto-wires from the service: https://symfony.com/doc/current/logging/channels_handlers.html#how-to-autowire-logger-channels

These messages will be included into the main log as well.

To exclude messages from the main log, add a channel exception in the section:

monolog:
channels: [ custom ]
handlers:
custom:
level: debug
type: stream
path: '%kernel.logs_dir%/custom.log'
channels: [ custom ]
xlite:
channels: [ '!custom' ]

Important considerations:

  • Do not create wrappers above logging, i.e. every time you need to write in a log, call for $this->getLogger()->... or static::getStaticLogger()->....
  • Using a custom logger, you can wrap it to auto-wire from a service.
  • Use getStaticLogger() only in static methods.
  • Add the trace (\Exception::getTrace()) field in the data when logging exceptions.
  • Pay attention to the logging levels:
/**
* Detailed debug information
*/
const DEBUG = 100;

/**
* Interesting events
*
* Examples: User logs in, SQL logs.
*/
const INFO = 200;

/**
* Uncommon events
*/
const NOTICE = 250;

/**
* Exceptional occurrences that are not errors
*
* Examples: Use of deprecated APIs, poor use of an API,
* undesirable things that are not necessarily wrong.
*/
const WARNING = 300;

/**
* Runtime errors
*/
const ERROR = 400;

/**
* Critical conditions
*
* Example: Application component unavailable, unexpected exception.
*/
const CRITICAL = 500;

/**
* Action must be taken immediately
*
* Example: Entire website down, database unavailable, etc.
* This should trigger the SMS alerts and wake you up.
*/
const ALERT = 550;

/**
* Urgent alert.
*/
const EMERGENCY = 600;

Use INFO instead of DEBUG in most of the cases. This will prevent from unnecessary backtrace when you only need to check the events flow. In case of an error, you must carefully select between NOTICE, WARNING and ERROR, because NOTICE will not be logged under the recommended settings, and the messages below ERROR won't have a backtrace.

  • Do not create a special debug mode for logging in add-ons, better use messages on the DEBUG or INFO levels. Sometimes in the debug mode it may be required to send extra data to external server. If this is your case, use the current logging level (\XLite\Logger::getCurrentLevel) or create an extra setting.

CloudWatch

Use ENV variable to set up CloudWatch:

# Amazon AWS CloudWatch configuration and credentials
LOGGER_CLOUD_WATCH_REGION=eu-west-1
LOGGER_CLOUD_WATCH_VERSION=latest
LOGGER_CLOUD_WATCH_KEY=
LOGGER_CLOUD_WATCH_SECRET=
LOGGER_CLOUD_WATCH_TOKEN=
LOGGER_CLOUD_WATCH_GROUP_NAME=
LOGGER_CLOUD_WATCH_STREAM_NAME=xlite
LOGGER_CLOUD_WATCH_RETENTION_DAYS=30

Service Tool

Service tool logging is done using the built-in Symfony services. Logs are parsed as they do in XC.

  • Log file: {root}/var/log/{year}/{month}/xlite.log{year}-{month}-{day}.service-tool.php
  • If CloudWatch is configured, it receives the service tool messages as well.

Hooks

  • init - run on every request to XC
  • install - run after the add-on installation
  • enable - run after the add-on is enabled (after install, in case of installation)
  • disable - run after the add-on is disabled
  • remove - run only during deletion of an add-on that cannot be disabled (that has canDisable:false in main.yaml)
  • rebuild - run after every rebuild (after install and enable)
  • upgrade - run during upgrade (before rebuild)

To store hooks, use separate classes that are DI services.

To subscribe to every hook, add a corresponding tag to the service description:

modules/[Author]/[Name]/Resources/config/services.yaml

[Author]\[Name]\LifetimeHook\Hook:
tags:
- {name: xcart.lifetime-hook, type: init, method: onInit}
- {name: xcart.lifetime-hook, type: install, method: onInstall}
- {name: xcart.lifetime-hook, type: enable, method: onEnable}
- {name: xcart.lifetime-hook, type: disable, method: onDisable}
- {name: xcart.lifetime-hook, type: rebuild, method: onRebuild}
- {name: xcart.lifetime-hook, type: upgrade, method: onUpgrade, version: '5.5.0.1'}

An add-on can have several hook classes, if it is required for a better code readability. For example, upgrade hooks can be moved to a separate class, or every hook can have a separate class.

Packing an add-on

It is possible to pack an add-on only by using the colsole command ./bin/service xcst:pack-module.

Below is a brief reference for how to use this command.

Description

Creates a module package.

Help

Creates a module package that can be uploaded to the Marketplace or installed in a store.

Options:

  • -source, s - The code source: git or local. If local is selected, the package is compiled from the current code. If git is selected, the package is compiled from the current git branch.
  • -modules, m - A comma-separated list of modules for the package. Each module is a separate package. Modules are listed in the {AuthorId}-{ModuleId} format.
  • -all-modules, a - Creates all modules package.

For example:

./bin/service xcst:pack-module --source=git --modules=CDev-Catalog,CDev-Egoods

./bin/service xcst:pack-module --source=git --all-modules