Sylius custom theme development – initial setup

Related Inchoo Services

This post is for you if you are a frontend developer working on Sylius eCommerce platform and you want to build a custom theme for your client. Sylius is mostly oriented towards backend developers and frontend part is only necessary to show what can be built on top of the platform. Frontend segments don’t come with much documentation or guidance, and what is available will help you with the kick start, but after that, you are on your own.

But wait, please, don’t be scared, this platform is actually very easy to use and modify once you master it. In the end you will be able to create anything what you or your client wants. In the very near feature in one of the next releases we are looking forward to seeing a new default theme for Sylius that has all out of the box functionalities which will show the true power of Sylius and what can be built on top of it.

In the meantime, read on…

Currently, the default theme you’ll find has rather basic eCommerce features, it is built using Semantic UI and it is a nice framework but if you are going to build a custom theme then it is very much possible that it will be only a burden which you will need to get rid of. Later on, the best approach will be to remove the framework altogether and update the HTML template, sorry, twig templates, and add your own classes and rearrange the order of HTML elements.

But first things first, after you install Sylius, which is really simple, and following documentation for admin and frontend part you need to have installed node.js and yarn package manager, in most cases, to install yarn you will need to have the npm package manager installed :-).

The main reason to have and use node/yarn is that you need to build, generate, static files and assets like css, js, copy images (not product images, logo, icons, background images etc). Let’s look into documentation and comment a few steps. You can find the documentation here under Sylius installation.

Installing assets

In order to see the fully functional frontend you will need to install its assets.

Ok, here we go, please note, you need to be familiar with node.js and using its package system and also you need to use at least one “task runner” package, gulp, grunt etc. Sylius uses gulp for building frontend. Without this step you will end up with only markup without styling and you will have “ugly” admin and frontend.

To be sure that you have all the requirements, open termin/console, type, “yarn -v” and “gulp -v” or go and pay a visit to https://gulpjs.com/ and https://yarnpkg.com/lang/en/ and follow the installation procedure.

Move on to the next step.

Please examine package.json file in root of the project. There is a list of packages that will be installed after you hit “yarn install“.

For example “dependencies”

"babel-polyfill": "^6.26.0",
"jquery": "^3.2.0",
"lightbox2": "^2.9.0",
"semantic-ui-css": "^2.2.0"

Just a short comment here, you can see that we have a few “externally” imported asset files like lightbox or jQuery, and Semantic UI CSS. So, in theory, if you want to remove lightbox and use something else you will need to look inside node_modules folder :-).

Also there are “devDependencies”, like gulp, babel, eslint and similar.

In the root of the project we have gulpfile.babel.js file.

import chug from 'gulp-chug';
import gulp from 'gulp';
import yargs from 'yargs';
 
const { argv } = yargs
  .options({
    rootPath: {
      description: '<path> path to public assets directory',
      type: 'string',
      requiresArg: true,
      required: false,
    },
    nodeModulesPath: {
      description: '<path> path to node_modules directory',
      type: 'string',
      requiresArg: true,
      required: false,
    },
  });
 
const config = [
  '--rootPath',
  argv.rootPath || '../../../../public/assets',
  '--nodeModulesPath',
  argv.nodeModulesPath || '../../../../node_modules',
];
 
export const buildAdmin = function buildAdmin() {
  return gulp.src('src/Sylius/Bundle/AdminBundle/gulpfile.babel.js', { read: false })
    .pipe(chug({ args: config, tasks: 'build' }));
};
buildAdmin.description = 'Build admin assets.';
 
export const watchAdmin = function watchAdmin() {
  return gulp.src('src/Sylius/Bundle/AdminBundle/gulpfile.babel.js', { read: false })
    .pipe(chug({ args: config, tasks: 'watch' }));
};
watchAdmin.description = 'Watch admin asset sources and rebuild on changes.';
 
export const buildShop = function buildShop() {
  return gulp.src('src/Sylius/Bundle/ShopBundle/gulpfile.babel.js', { read: false })
    .pipe(chug({ args: config, tasks: 'build' }));
};
buildShop.description = 'Build shop assets.';
 
export const watchShop = function watchShop() {
  return gulp.src('src/Sylius/Bundle/ShopBundle/gulpfile.babel.js', { read: false })
    .pipe(chug({ args: config, tasks: 'watch' }));
};
watchShop.description = 'Watch shop asset sources and rebuild on changes.';
 
export const build = gulp.parallel(buildAdmin, buildShop);
build.description = 'Build assets.';
 
gulp.task('admin', buildAdmin);
gulp.task('admin-watch', watchAdmin);
gulp.task('shop', buildShop);
gulp.task('shop-watch', watchShop);
 
export default build;

And right here is the place where the magic is happening. After you hit “yarn build” it will run task “build” line 52, which have two subtasks buildAdmin (line 28) and buildShop (line 40). After this you will have installed all the assets needed for frontend and admin interface. Let’s look a bit closer at lines 55 to 58 where you can find two other tasks, beside build there are also tasks named watch, watchAdmin and watchShop. These tasks can be used if you are trying to edit for example the background color of admin area, it will watch changes inside .sass file and compile to .css.

Ok now dive deeper, task buildShop line 41 points to a new file src/Sylius/Bundle/ShopBundle/gulpfile.babel.js.

Now here is where things get more interesting.

import { rollup } from 'rollup';
import { uglify } from 'rollup-plugin-uglify';
import babel from 'rollup-plugin-babel';
import commonjs from 'rollup-plugin-commonjs';
import concat from 'gulp-concat';
import dedent from 'dedent';
import gulp from 'gulp';
import gulpif from 'gulp-if';
import inject from 'rollup-plugin-inject';
import livereload from 'gulp-livereload';
import merge from 'merge-stream';
import order from 'gulp-order';
import resolve from 'rollup-plugin-node-resolve';
import sass from 'gulp-sass';
import sourcemaps from 'gulp-sourcemaps';
import uglifycss from 'gulp-uglifycss';
import upath from 'upath';
import yargs from 'yargs';
 
const { argv } = yargs
  .options({
    rootPath: {
      description: '<path> path to web assets directory',
      type: 'string',
      requiresArg: true,
      required: true,
    },
    vendorPath: {
      description: '<path> path to vendor directory',
      type: 'string',
      requiresArg: true,
      required: false,
    },
    nodeModulesPath: {
      description: '<path> path to node_modules directory',
      type: 'string',
      requiresArg: true,
      required: true,
    },
  });
 
const env = process.env.GULP_ENV;
const options = {
  minify: env === 'prod',
  sourcemaps: env !== 'prod',
};
 
const rootPath = upath.normalizeSafe(argv.rootPath);
const shopRootPath = upath.joinSafe(rootPath, 'shop');
const vendorPath = upath.normalizeSafe(argv.vendorPath || '.');
const vendorShopPath = vendorPath === '.' ? '.' : upath.joinSafe(vendorPath, 'ShopBundle');
const vendorUiPath = vendorPath === '.' ? '../UiBundle/' : upath.joinSafe(vendorPath, 'UiBundle');
const nodeModulesPath = upath.normalizeSafe(argv.nodeModulesPath);
 
const paths = {
  shop: {
    js: [
      upath.joinSafe(vendorUiPath, 'Resources/private/js/**'),
      upath.joinSafe(vendorShopPath, 'Resources/private/js/**'),
    ],
    sass: [
      upath.joinSafe(vendorUiPath, 'Resources/private/sass/**'),
      upath.joinSafe(vendorShopPath, 'Resources/private/sass/**'),
    ],
    css: [
      upath.joinSafe(nodeModulesPath, 'semantic-ui-css/semantic.min.css'),
      upath.joinSafe(nodeModulesPath, 'lightbox2/dist/css/lightbox.css'),
      upath.joinSafe(vendorUiPath, 'Resources/private/css/**'),
      upath.joinSafe(vendorShopPath, 'Resources/private/css/**'),
      upath.joinSafe(vendorShopPath, 'Resources/private/scss/**'),
    ],
    img: [
      upath.joinSafe(vendorUiPath, 'Resources/private/img/**'),
      upath.joinSafe(vendorShopPath, 'Resources/private/img/**'),
    ],
  },
};
 
const sourcePathMap = [
  {
    sourceDir: upath.relative('', upath.joinSafe(vendorShopPath, 'Resources/private')),
    destPath: '/SyliusShopBundle/',
  },
  {
    sourceDir: upath.relative('', upath.joinSafe(vendorUiPath, 'Resources/private')),
    destPath: '/SyliusUiBundle/',
  },
  {
    sourceDir: upath.relative('', nodeModulesPath),
    destPath: '/node_modules/',
  },
];
 
const mapSourcePath = function mapSourcePath(sourcePath) {
  const match = sourcePathMap.find(({ sourceDir }) => (
    sourcePath.substring(0, sourceDir.length) === sourceDir
  ));
 
  if (!match) {
    return sourcePath;
  }
 
  const { sourceDir, destPath } = match;
 
  return upath.joinSafe(destPath, sourcePath.substring(sourceDir.length));
};
 
export const buildShopJs = async function buildShopJs() {
  const bundle = await rollup({
    input: upath.joinSafe(vendorShopPath, 'Resources/private/js/app.js'),
    plugins: [
      {
        name: 'shim-app',
 
        transform(code, id) {
          if (upath.relative('', id) === upath.relative('', upath.joinSafe(vendorShopPath, 'Resources/private/js/app.js'))) {
            return {
              code: dedent`
                import './shim/shim-polyfill';
                import './shim/shim-jquery';
                import './shim/shim-semantic-ui';
                import './shim/shim-lightbox';
 
                ${code}
              `,
              map: null,
            };
          }
 
          return undefined;
        },
      },
      inject({
        include: `${nodeModulesPath}/**`,
        modules: {
          $: 'jquery',
          jQuery: 'jquery',
        },
      }),
      resolve({
        jail: upath.resolve(nodeModulesPath),
      }),
      commonjs({
        include: `${nodeModulesPath}/**`,
      }),
      babel({
        babelrc: false,
        exclude: `${nodeModulesPath}/**`,
        presets: [
          ['env', {
            targets: {
              browsers: [
                'last 2 versions',
                'Firefox ESR',
                'IE >= 9',
                'Android >= 4.0',
                'iOS >= 7',
              ],
            },
            modules: false,
            exclude: [
              'transform-async-to-generator',
              'transform-regenerator',
            ],
            useBuiltIns: true,
          }],
        ],
        plugins: [
          ['external-helpers'],
          ['fast-async'],
          ['module-resolver', {
            alias: {
              'sylius/ui': upath.relative('', upath.joinSafe(vendorUiPath, 'Resources/private/js')),
            },
          }],
          ['transform-object-rest-spread', {
            useBuiltIns: false,
          }],
        ],
      }),
      options.minify && uglify(),
    ],
    treeshake: false,
  });
 
  await bundle.write({
    file: upath.joinSafe(shopRootPath, 'js/app.js'),
    format: 'iife',
    sourcemap: options.sourcemaps,
  });
};
buildShopJs.description = 'Build shop js assets.';
 
export const buildShopCss = function buildShopCss() {
  const copyStream = merge(
    gulp.src(upath.joinSafe(nodeModulesPath, 'semantic-ui-css/themes/**/*'))
      .pipe(gulp.dest(upath.joinSafe(shopRootPath, 'css/themes'))),
  );
 
  const cssStream = gulp.src(paths.shop.css, { base: './' })
    .pipe(gulpif(options.sourcemaps, sourcemaps.init()))
    .pipe(concat('css-files.css'));
 
  const sassStream = gulp.src(paths.shop.sass, { base: './' })
    .pipe(gulpif(options.sourcemaps, sourcemaps.init()))
    .pipe(sass())
    .pipe(concat('sass-files.scss'));
 
  return merge(
    copyStream,
    merge(cssStream, sassStream)
      .pipe(order(['css-files.css', 'sass-files.scss']))
      .pipe(concat('style.css'))
      .pipe(gulpif(options.minify, uglifycss()))
      .pipe(gulpif(options.sourcemaps, sourcemaps.mapSources(mapSourcePath)))
      .pipe(gulpif(options.sourcemaps, sourcemaps.write('./')))
      .pipe(gulp.dest(upath.joinSafe(shopRootPath, 'css')))
      .pipe(livereload()),
  );
};
buildShopCss.description = 'Build shop css assets.';
 
export const buildShopImg = function buildShopImg() {
  const copyStream = merge(
    gulp.src(upath.joinSafe(nodeModulesPath, 'lightbox2/dist/images/*'))
      .pipe(gulp.dest(upath.joinSafe(shopRootPath, 'images'))),
  );
 
  return merge(
    copyStream,
    gulp.src(paths.shop.img)
      .pipe(gulp.dest(upath.joinSafe(shopRootPath, 'img'))),
  );
};
buildShopImg.description = 'Build shop img assets.';
 
export const watchShop = function watchShop() {
  livereload.listen();
 
  gulp.watch(paths.shop.js, buildShopJs);
  gulp.watch(paths.shop.sass, buildShopCss);
  gulp.watch(paths.shop.css, buildShopCss);
  gulp.watch(paths.shop.img, buildShopImg);
};
watchShop.description = 'Watch shop asset sources and rebuild on changes.';
 
export const build = gulp.parallel(buildShopJs, buildShopCss, buildShopImg);
build.description = 'Build assets.';
 
export const watch = gulp.parallel(build, watchShop);
watch.description = 'Watch asset sources and rebuild on changes.';
 
gulp.task('shop-js', buildShopJs);
gulp.task('shop-css', buildShopCss);
gulp.task('shop-img', buildShopImg);
gulp.task('shop-watch', watchShop);
 
export default build;

It will be hard to go and comment each line but I will try to present the short version :-). I forgot to mention that if you cd navigate to this folder “src/Sylius/Bundle/ShopBundle” you can use tasks from starting from line 253 independent, standalone, maybe you have changed on theme few CSS, Sass lines you don’t need to build the assets over and over again using “yarn bulid” naturally when you are still in development.

If you want to add some custom Sass files which will be processed then you can use a folder “Resources/private/sass/” for example, and if you want to change something on Semantic UI then go to node_modules and look for “semantic-ui-css” folder.

Ok – back to the file. The main idea is to define source folder and destination folders, if you change something inside source folders it will take up those changes, process them, merge and copy to “web” folder inside Sylius project root. There are also a few additional tools like http://livereload.com/ for live refreshing the browser after you have made the needed changes. In the next post I will show you another build script where you can use browsersync for observing the changes.

So this is happening when you hit “Yarn install” and “Yarn build” inside terminal after this you can type

php bin/console server:start --docroot=web 127.0.0.1:8000

Open 127.0.0.1:8000 inside your browser and you will have Sylius shop ready, with all of the assets installed :-).

What’s next?

This post should serve as a quick reference and a reminder to help you understand the current Sylius theme development workflow. It is not a standard or the only way, and the beauty of Sylius is that you can use and build your own theme in million different ways, this is only one small part of the whole ecosystem.

In my next posts I will try to explain the concept of creating and developing custom themes.

If you notice a mistake somewhere in the code or elsewhere in this article or if you have any questions, please comment below.

Good luck with your Sylius projects!

You made it all the way down here so you must have enjoyed this post! You may also like:

Sylius custom theme development – Setting up workflow powered with webpack and Symfony 4 best practices Luka Omrcen
, | 0

Sylius custom theme development – Setting up workflow powered with webpack and Symfony 4 best practices

Sylius custom theme development – Create a theme Stanislav Mihic
, | 0

Sylius custom theme development – Create a theme

Creating Sylius Fixtures Toni Pap
Toni Pap, | 0

Creating Sylius Fixtures

Leave a Reply

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes: <a href="" title=""> <blockquote cite=""> <code> <del datetime=""> <em> <s> <strike> <strong>. You may use following syntax for source code: <pre><code>$current = "Inchoo";</code></pre>.