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.
Download the X-Cart 5.5.0.x distribution package.
Extract the package on the server.
tar -xzpf x-cart-5.5.0.0-platform-en.tar.gz
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
andXCART_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.
- 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
- 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 (Theadmin
,customer
andcommon
folders have been moved to theweb
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 (Theadmin
,customer
andcommon
folders have been moved to theweb
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 namethis
. - 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 theget
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 theMixin
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\*
andLC_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()->...
orstatic::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
orINFO
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