Author: Matthias Walter

  • Chrome 63 vs. dev domains

    Chrome 63 vs. dev domains

    With the recent update to version 63 for Chrome they introduced a feature, that redirects all *.dev  calls from HTTP to HTTPS.

    If you are using *.dev  domains to access your development machine, this behaviour is probably not what you want for all your local urls.

    Configuring dnsmasq

    At netz98 we are using dnsmasq to redirect all *.dev  calls to our localhost.

    dnsmasq has a setting where you can easily change this to another domain like *.localhost .

    First of locate the config file for dnsmasq. If you have installed it using brew you will probably find it under /usr/local/etc/dnsmasq.conf .

    Open the file in an editor of your choice and look for the following line:

    address=/dev/127.0.0.1

    And change it to:

    address=/localhost/127.0.0.1

    If you cannot find a line setting the address like shown above, just add a new line with the setting.

    To restart dnsmasq you can use

    brew services restart dnsmasq

    Configuring apache / nginx

    Depending on your choice of setup you might be using an nginx or apache setup.

    You most likely have some virtual-hosts or server settings that might need to be adjusted to the new domain.

    This could look something like this for nginx.

    server {
      listen 80;
    
      server_name shop.dev;
    
      // ....
    }

    Locate the server_name setting and any other occurences of your *.dev  domain and change it to .localhost

    Do not forget to change your Magento1 or Magento2 Urls in the core_config_data table.

    MacOS Nameserver

    For Firefox and Safari and a working ping foo.bar.localhost , you also have to add a setting for that.

    Create the file /etc/resolver/localhost  with the following content:

    nameserver 127.0.0.1

    After doing so, you need to restart the DNS Resolver and flush caches.
    I am on the latest MacOS 10.13 so I had to run the following command:

    sudo killall -HUP mDNSResponder; sudo killall mDNSResponderHelper; sudo dscacheutil -flushcache

    If you are running a different MacOS version, you can find a great blog post over at Dreamhost where they list the commands for each MacOS version: https://help.dreamhost.com/hc/en-us/articles/214981288-Flushing-your-DNS-cache-in-Mac-OS-X-and-Linux

    UPDATE:
    You might have to do a reboot of your machine so that the resolver works.

    UPDATE 2:
    Firefox and Safari require the above mentioned resolver to work.

    If you got anything to add or there even is a better solution feel free to leave a comment below.

  • Recap Meet Magento 2017 Germany

    Recap Meet Magento 2017 Germany

    After attending my 10th Meet Magento in Germany (yes I missed one) and coming back to work, I think it is time for a short recap.
    This years Meet Magento probably was the biggest one in terms of number of attendants (800, according to the organizers).
    The location was at it’s limits for that amount of people.
    Luckily the weather was sunny so people could get outside and get some space.
    The next years Meet Magento will probably / hopefully be taking place in a better suited venue.
    Besides that I had a lot of interesting conversations to old as well as new friends and contacts.
    Mostly, I attended technical talks, for some of those you will get a short recap in this post.

    Pre-Party

    But before starting with that, I have to mention the Pre-Party.
    Although I have been attending the Meet Magento for quite some time now, I have never made it to the Pre-Party.
    And after attending this years Pre-Party, I have to say that will change from now on 😉
    The location was nice and it was a great way to catch up with everyone and get into the conference flow.

    Magento Product Portfolio & Roadmap

    After the opening by the Meet Magento organizers, Paul Boisvert (Head of Product Management at Magento) held his keynote.
    Sharing the vision for Magento and the achievements of the community and Magento itself.
    He summarised the achievements during the Contribution Days at different events.

    The most important part that we probably all like, is the change of their philosophy regarding the open source strategy.
    In the past years Magento was delivered Open-Source, but there was no process or workflow for the community to contribute.
    That changed in the recent months as you probably already noticed.
    Magento is moving from Distribution to Contribution, which I think is the right thing to do.

    You can already see quite some pull-requests being merged.
    And during the Hackathon in Leipzig the community was working on the MNI – Multi Node Inventory module.

    Magento 2 in Production

    Marcel Hauri of Stämpfli gave his talk about his experience in running Magento in a production environment.
    He summarized the issues he experienced while bringing and running Magento 2 in production.
    Some of the modules and tools he created can be found on github: https://github.com/staempfli
    His slides are a available here:
    https://speakerdeck.com/mhauri/mm17de-magento-2-in-production

    12 Ways to Improve Magento 2 Security and Performance

    Next up was Pavlo Okhrem giving his tips and insights on how to improve the security and performance of a Magento 2 instance.
    He shared some settings for PHP and Nginx to improve the performance.

    Besides that he gave a summary of points to improve the security, by limiting the access to the repository and server in different ways.

    Magento 2 Best Practice Workflow

    David Lambauer – an old colleague at netz98 – had is talk presenting a best practice workflow when developing for Magento.
    He showed us how he is splitting up his code and how he is structuring that.

    In my opinion, the most important part is to think before you code.
    It is faster to think before hand and will payback afterwards, because when handling big tasks it is always good to have an overview of what you want to achieve as a final goal.
    Furthermore if you do that in writing you have a plan on what to do and you don’t have to remember everything.
    David also gave as an overview of the different types of tests that can be written to improve the quality of your code.

    And at the end he shared a module skeleton he published on github:
    https://github.com/AOEpeople/Magento-2-Module-Skeleton

    The presentation was great to attend to and the workflows and approaches he described are great to produce high quality software.
    I wonder where he learned all of that 😉

    Magento Commerce: Business Momentum & Vision

    The keynote given by Magento CEO Mark Lavelle gave as a insights on the current status of Magento and where Magento expects to be in the future.
    Furthermore we got an overview of the future Magento ecosystem and which systems are already included, which will be added and how the will be integrated.

    Test Driven Middleware

    This technical talk by Andreas Gies was about a middleware, how the middleware is built and how it is tested.
    In almost every commerce project I have done over the years, there are always multiple systems and services involved.
    It all comes to down to how they are integrated and tested.
    This talk was about connecting those systems with a middleware in an high performance environment.
    The architecture of the system looks like this.
    Andreas also described how he simulates the messaging between the systems in a testing setup using docker containers.
    Though it was not a directly PHP or Magento related talk, I still got some useful insights.

    SUPER-scaling E-Commerce with Magento

    Fabricio Branca presenting his approach on SUPER-scaling an e-commerce project where Magento is involved.
    We got a good overview of the general system architecture and an idea about the approach to extract parts of the systems and replace them through other services.
    Basically each system provides a number of services to the construct.
    The systems are inter connected by REST-APIs. By using APIs we retain the possibility to exchange services whenever needed.

    The deployment of new release for those services / systems in an cloud-based environment was also part of his presentation.

    Magento Security Best Practices

    After that Anna Völkl gave a practical overview of ways to improve the security of your Magento 1 and 2 systems.

    She listed and described quite some modules and tools to improve the overall security of your Magento shops.

    But security not only concerns the system itself, but also the way you treat the data, like when creating db-dumps.
    You can use Magerun (1/2) to create database-dumps while stripping customer specific data from those dumps.

    And she mentioned even more tools, which some of them I haven’t heart of till now.
    It is definitely worth to check those out:

    Building a Framework Agnostic API

    Then it was time for David Manners to explain his approach on how to un-magento your code.

    The basic idea is to separate your business logic and describe it through interface.
    Especially when there is system specific actions involved like retrieving stuff from the database or getting some config-values.

    I liked his view on things, as we have been following a quite similar approach for years now.
    Not in terms of keeping code working in Magento 1 and 2 at the same time, but more like how to separate code into different levels and keeping system-independent.

    David created a sample of his approach and published that on github:
    https://github.com/sitewards/setup
    https://github.com/sitewards/setup-mage1
    https://github.com/sitewards/setup-mage2

    Talks missed

    Sadly there where some talks I would have loved to attend but I had to choose one.

    After-Show-Party

    It is always a pleasure to part of the The Meet Magento After-Show-Party.
    The party is well organized and the food is great.
    But since there were that much attendants of the conference this year, the party was packed to.
    And the location was getting to its limits as well, although that was only really an issue during dinnertime.

    Here are some impressions of that party.

    https://twitter.com/_dermatz/status/866713698697654273

    SUMMARY

    The Meet Magento in Leipzig was a great event with lots of good talks and discussions.
    I would have loved some more technical talks, because it seems the audience was there.
    Every technical related talk had a full packed room.
    Maybe we will see the next Meet Magento taking place in a bigger location with even more talks 🙂

    See you around.

  • Introducing MageDeploy2

    Introducing MageDeploy2

    In our recent post series about Deploying Magento2 using Jenkins and deployer I was showing you how our Deployments are set up.

    In case you haven’t read them and are interested in the details here are the links:

    During the time of writing those articles I realized quite some improvements and generalizations that could be done to make this deployment more maintainable, extensible and customizable. I wanted to have a deployment setup that allows local execution with colored output, execution on a build server without interaction and usage in a build pipeline.
    Furthermore I wanted the deployment setup not only to be usable within netz98 but also by the whole Magento community.

    What I came up with I called MageDeploy2 which I will introduce with this post.

    If you read the previous post you will probably remember the diagrams showing the actions executed on the particular servers. I used one of those to mark the areas which will be provided by the MageDeploy2 setup.

    Now let’s go into details on how those phases and steps are implemented and what you need to get started with a PUSH deployment for Magento2 yourself.

    About MageDeploy2

    MageDeploy2 combine’s multiple technologies and open-source projects to provide the deployment setup.
    It basically is a set of tools, configurations files, some custom tasks for Robo and Deployer, all tailored to fit the needs of deploying a Magento2 project.

    For those new to Robo and Deployer:

    • Robo is a task runner that allows you to write fully customizable tasks in common OOP PHP style http://robo.li/
    • Deployer is Deployment tool for php, which follows a approach similar to capistrano https://deployer.org/

    I will not go into to much detail on how those tools work, you can get that from their designated websites and documentation.

    MageDeploy2 can be divided into 3 phases that can each be triggered separately.

    • magento-setup (preparing a local magento-setup)
    • artifacts-generate (generating the assets and packaging them)
    • deploy (release to production environment)

    Those phases are implemented as commands in the RoboFile.

    MageDeploy2 is divided into different packages that are installed when installing through composer.

    • mwltr/robo-deployer : contains Robo-Tasks for deployer
    • mwltr/robo-magento2 : contains Magento2 specific Robot-Tasks

    Those Robo-Tasks are not a full set of all possible commands and options but currently offer the commands and modifiers needed in deployment scenario. They are decoupled and can be re-used in other projects.

    As far as the deployer setup is concerned, MageDeploy2 uses n98/n98-deployer to include deployer configurations and tasks, them being:

    • set of Magento2 specific tasks
    • Magento2 Default Recipe
    • RoleManager for servers
    • optimized deployer standard tasks

    Requirements

    As I mentioned earlier, Magento2 Deployment Setup is using Robo to control the local setup and the overall deployment process. To achieve the actual deployment to the distinct environment it comes with a pre-configured Deployer setup. Please note that using Deployer is not mandatory, you can use whatever tool you like.

    It also expects that you have a git repository available, where you have commited your Magento2 composer.json file in either the root or in a sub-directory. Right now we are only supporting git but it should not be that big of a problem to connect to another VCS.
    Finally you need to have configured the access to the Magento composer repository for your current user.

    Create a new Deployment

    To Create a new deployment setup just run the following command.

    composer create-project mwltr/magedeploy2-base <dir>

    Note: Robo needs to be installed using composer, otherwise the usage of custom Tasks is not available. See the Robo Documentation Including Additional Tasks

    Configuration

    After the Installation you have to edit the magedeploy2.php and the deploy.php file to suit your needs. MageDeploy2 assumes you have a git repository containing the magento composer.json. Furthermore your local build environment can clone said repository and download the Magento packages using composer.

    MageDeploy2 Configuration

    To configure the MageDeploy2 use the following command:

    ./vendor/bin/robo config:init

    It will guide you throught the most important configuration options. Don’t worry you can edit the magedeploy2.php later-on.

    Next, run

    ./vendor/bin/robo validate

    to validate your build environment is setup.

    Setup local build environment

    If you are done with the configuration in magedeploy2.php, you can see if your build environment can be setup. To do so run this command:

    ./vendor/bin/robo deploy:magento-setup develop

    You can use a different branch or tag depending on your git repository setup.

    After the magento-setup has run successfully, you can now generate the assets by running the command:

    ./vendor/bin/robo deploy:artifacts-generate

    After this command is complete you should see the packages beneath shop.

    At this point we are sure that the local build setup is working and we can now continue with releasing our project.

    Deployer Configuration

    To evaluate we will create a local deployment target. To do so copy the local.php.dist by runing

    cp config/local.php.dist config/local.php

    and set the config values according to your local deploy target.

    Check the configuration in deploy.php and adjust it to your requirements. The default configurations and tasks are defined in \N98\Deployer\Recipe\Magento2Recipe. You can also have a look at all the configurations available in the Deployer Documentation

    Setting up deploy directory tree

    After you are done with setting the configuration, you can now initialize the directory tree of the deploy target run

    ./vendor/bin/dep deploy:prepare local

    This will create the required directories on your local deploy target.

    Setting up deploy target (optional)

    If you want to set up your deploy target as well you can use the command

    ./vendor/bin/dep server:setup local

    It will make an initial deployment to push your code to the deploy target.

    When this is done navigate to your local deploy_path and run the magento install command to setup the database. This might look something like this:

    cd <deploy_path>
    php bin/magento setup:install --db-host=127.0.0.1 --db-name=magedeploy2_dev_test_1_server --db-user=root --admin-email=admin@mwltr.de 
    --admin-firstname=Admin --admin-lastname=Admin --admin-password=admin123 --admin-user=admin 
    --backend-frontname=admin --base-url=http://magedeploy2_dev --base-url-secure=https://magedeploy2_dev 
    --currency=EUR --language=en_US --session-save=files --timezone=Europe/Berlin --use-rewrites=1

    Now we have Magento database and configuration on our deploy target and are ready to continue with the final step.

    Deploying the project

    At this point, you have setup the build environment and target environment and can finally start with the actual deployment. You can do so by running:

    ./vendor/bin/dep deploy local

    Congrats you have successfully setup your deployment pipeline and run the first deployment!

    Commands

    If you went through the tutorial above, you may have already used most of them.
    A full list of commands is available in the github repository here:
    https://github.com/mwr/magedeploy2-base#commands
    The following diagram shows the commands responsibility within the deployment pipeline.

    deploy:magento-setup

    Runs all tasks in the stage magento-setup. It will setup or update a local Magento instance by pulling the source-code from git, installing composer dependencies and installing or updating a local database.

    deploy:artifacts-generate

    Runs the Magento di:compile and setup:static-content-deploy commands to generate the assets. It is using your configuration from the magedeploy2.php.

    After generating those assets it will create packages, again according to your configuration.

    deploy:deploy

    This command will invoke deployer to release your project and push the prepared artifacts to the server.

    deploy

    Triggers the deployment with all it’s stages and can be considered to run deploy:magento-setup, deploy:artifacts-generate and deploy:deploy internally.

    Customization

    MageDeploy2 was designed to be highly customizable to suite your needs. Here are some areas that are easy to adjust:

    • Add or overwrite Robo-Tasks
    • Add or overwrite existing or additional configuration to MageDeploy2
    • Customize Deployer but still have the basic set of tasks available
    • Exchange deployer with a different tool

    The go into details here would exceed the purpose of this introduction. We may go into details in this area in a later post though.

    Final Words

    This is it, I hope you like the tool and it will be helpful setting up a PUSH deployment of your own.
    And as always let me know your thoughts and feedback in the comments below or contact me directly.

     

  • Deploying Magento2 – Future Prospects [4/4]

    Deploying Magento2 – Future Prospects [4/4]

    This post is part of series:

    Recap

    In the previous posts we dived into our Deployment Pipeline and the Release to the staging or production environments. You should check those posts first before reading this one.

    In this post we will share our thoughts on where we want to go with our deployment setup and what we have planned.

    To recall, this is our current Deployment Setup in a simplified overview:

    I have marked the phases the deployment is going through and the one important point in our deployment which is when all artifacts have been created and are available in the filesystem.
    This will be the key-point in our deployment setups, because we can have a clean cut here and switch /adjust the following phase based on our customers needs or the server infrastructural requirements.

    Our goal is to have a standard setup as far as possible and then be able to deploy to physical servers, cloud setups or even use completely different deployment approach.

    Preface

    The next paragraphs will be about the different setups we plan to serve with this deployment. Note that the following Deployment Setups are still under evaluation and are just stating my current thoughts on their specific area. Furthermore the diagrams shown below are superficial abstractions of the matter, so don’t expect to many details here.

    Optimising Artifact Generation

    Before we can continue to attach our deployment to different setups, there is one optimization I want to take in advance.
    At the moment we are generating multiple artifacts. A short reminder, these are the artifacts we are creating:

    config.tar.gz
    var_di.tar.gz
    var_generation.tar.gz
    pub_static.tar.gz
    shop.tar.gz
    

    To be more flexible in the future and to have a clean integration point (think of it like an Interface), I want to reduce the artifacts we create to exactly one.
    This should be possible but has not been implemented yet. It will be easier to extend and easier to understand if we have one artifact to continue with from there.
    Furthermore some setups might even require exactly one artifact so we would need it anyways.

    Deploying to platform.sh

    At the moment we are having some Magento2 projects delivered through platform.sh. The Deployment process and setup itself currently differs heavily from the previously described setup. Mainly because of historical reasons. At the time we had to create it, we still had our more or less our PULL & PUSH Setup described in the first post Deploying Magento2 &#8211; History &#038; Overview [1/4]. With our current platform.sh deployment we are still used jenkins, but mainly to trigger the build and deploy processes on the platform.sh side.
    That means that all processes are run on the platform.sh setup and thus directly pull from our gitlab or the Magento Composer Repository.

    This is not ideal due to speed issues we experience when compiling the assets in our platform.sh setup. Additionally we need to configure access to the netz98 Gitlab and Composer repository and of course the Magento Composer repository, as the composer install is run on the plaform.sh setup.
    To ease these situations we are tending to create a setup like this:

    As you can see, we are generating the assets and the artifact on our build server which is way faster than doing this in our platform.sh setup. If the artifact is available we will push that artifact to the git repository offered by platform.sh, thus triggering the actual deployment to the production environment.
    The final steps are to upgrade the production database, import config, control the release, cleanup, etc.

    In theory this should work, because we are just pushing code to platform.sh which is then used to run our application. We are planning to try this approach with the next platform.sh setup, probably in a months time. You can expect some post about our experience with this.

    Deploying to AWS using CodeDeploy

    We are working on AWS Cloud Deployments as well. With the approach we are following now should be able to deploy to a AWS Cloud Setup as well. We are evaluating different approaches to meet our customers requirements and still be cost effective.

    In this version we would deploy our code using AWS CodeDeploy which is taking care of updating the EC2 instances. The Database Upgrade would then be triggered on a admin EC2 instance which is not in the auto-scaling group.

    This is an example of how the deployment of the source-code / the application might look like. I know this is more like an easy setup, depending on the customers needs and budget this is one way to go.

    Deploy to AWS using ECS

    Deploying the source code to the EC2 Instances is one way to go. You can also use Amazon EC2 Container Service (in short Amazon ECS) to create Containers and deploy them to your EC2 instances. In short you are running one or more containers on you EC2 instances and control those containers through the Amazon ECS container registry.

    What we plan on doing here is creating the container image based on the artifacts we created using the standard deployment mechanism. This pre-build container image is then pushed to the Amazon ECS Registry. From there the deployment to the EC2 instances is controlled. The Container definition and the images to use for them is defined using Task Definitions. You can define multiple containers and the EC2 instances they shall be running on. The above overview is limited to the application deployment as this is the main target of this blog series. We might go in to more detail on our plans for different AWS Deployment setups with a more complete view.

    Deploying to …

    Thinking ahead, we might run into unexpected or complicated server environments. Following this push only approach we have a way that should be re-usable in most cases. Be it deploying with a restrictive VPN connection or to a highly secured server which does not allow a PULL.

    Summary

    This series was all about introducing our way of automatically deploying to our environments and how we got there. I hope you got a good understanding on the advantages of a PULL Deployment and you might achieve it yourself.

    As always, leave a comment if you got anything to add or to give us some feedback.

    Oh and …

    P.S.

    As I mentioned in my last post I am working on a default setup for Magento2 deployments. It is meant to be used as a starting point for custom deployments and helps you getting your automatic deployment pipeline up and running in a short amount of time. Futhermore I want to create a central point were issues or special constellations regarding the asset generation are handled.
    It will be configureable and highly customizable and it will contain some basic tasks that can be re-used.
    The project will be completely open-source and available via github.
    My next post will be a introduction to that Deployment, so stay tuned and leave a message here or ping me on twitter if you feel like it.

  • Deploying Magento2 – Releasing to Production [3/4]

    Deploying Magento2 – Releasing to Production [3/4]

    This post is part of series:

    Recap

    In the last post Jenkins Build-Pipeline Setup we had a look at our Jenkins Build-Pipeline and how to the setup and configuration is done. If you haven’t read it yet you should probably do so before reading this post.
    The last step in our Build-Pipeline was the actual Deployment which can be defined like this:

    stage 'Deployment'
    if (DEPLOY == 'true') {
        sh "./dep deploy --tag=${TAG} ${STAGE}"
    }

    You may notice the missing sshagent call compared to the previous post. This sshagent call results from one of our older deployment setups where we were still pulling code from the server. After writing the post about our Build-Pipeline setup I questioned that, and as it turns out we don’t need that anymore and can simplify our Deployments. This part was actually not so trivial to setup if you don’t know exactly what to do and what to look for, so I am happy to scratch that complexity.

    In this post we will dive into the actual Deployment and Rollout of your Magento2 application.

    Remembering the visualization of your Deployment Process, we are now enter the last action-block. I have marked the part we are going to elaborate accordingly.

    Prerequisites

    In the stage ‘Asset Generation’ we build all the necessary assets and created tar.gz files for them.
    Thus before starting stage ‘Deployment’ we have the following files available in the workspace of our Jenkins Build.

    config.tar.gz
    var_di.tar.gz
    var_generation.tar.gz
    pub_static.tar.gz
    shop.tar.gz

    Next up, those files will be used to build the release on the remote server.

    Starting the Deploy

    As mentioned in the last post we are using Deployer here to handle the release to staging or production environments.

    ./dep deploy --tag=${TAG} ${STAGE}

    The TAG and STAGE environment variables are set by Jenkins and defined for each Build, before starting the actual Build.
    A possible command might state like this:

    ./dep deploy --tag=3.2.0.1 production

    This call will rollout the release with the tag 3.2.0.1  to the production  environment.
    Though our deployer setup is no longer making a git connection we are providing the tag here to identify the release later on.

    Deployer Setup

    So this is how our Deploy Repository is setup:

    Here you can also see the Jenkinsfile defining the Build-Pipeline. We have a config directory containing the configurations for our environments. Including a possible local setup. The local setup is really helpful when improving or upgrading the deployment.

    In our deploy repository we have a composer.json to manage the necessary dependencies for the deployment. Them being deployer itself and our own set of tasks. Having our tasks in a dedicated repository gives us the possibility to share those tasks through out all deployments. That’s one thing I didn’t like with the default deployer approach.

    "require": {
        "deployer/deployer": "^4.0",
        "n98/lib.n98.framework.deployer": "^1.0.0"
    }

    deploy.php

    Let’s take a look at the deploy.php file that defines the configuration and tasks that are necessary for our deployment. We will go into more Detail afterwards.

    <?php
    /**
     * @copyright Copyright (c) 1999-2016 netz98 GmbH (http://www.netz98.de)
     *
     * @see PROJECT_LICENSE.txt
     */
    namespace Deployer;
    
    use N98\Deployer\Registry;
    use N98\Deployer\Task\BuildTasks;
    use N98\Deployer\Task\CleanupTasks;
    use N98\Deployer\Task\DeployTasks;
    use N98\Deployer\Task\MagentoTasks;
    use N98\Deployer\Task\SystemTasks;
    
    require 'recipe/common.php';
    
    /**
     * CONFIGURATION
     */
    $sharedFiles = [
        'src/app/etc/env.php',
    ];
    set('shared_files', $sharedFiles);
    
    $sharedDirs = [
        'src/pub/media',
        'src/var/log',
        'src/var/session',
        'src/var/composer_home',
        'src/var/n98_integration',
    ];
    set('shared_dirs', $sharedDirs);
    
    set('writable_dirs', []);
    set('keep_releases', 6);
    set('ssh_type', 'native');
    set('bin/n98_magerun2', 'n98-magerun2');
    set('webserver-user', 'www-data');
    set('webserver-group', 'www-data');
    set('phpfpm_service', 'php7.0-fpm');
    set('nginx_service', 'nginx');
    
    /**
     * SERVERS
     */
    $configLocal = __DIR__ . '/config/local.php';
    if (is_file($configLocal)) {
        require_once $configLocal;
    }
    require_once __DIR__ . '/config/staging.php';
    require_once __DIR__ . '/config/production.php';
    
    /**
     * TASKS & CONFIG
     */
    Registry::register();
    
    /**
     * DEPLOYMENT PIPELINE
     */
    desc('Deploy Project');
    task(
        'deploy', [
            'deploy:initialize',
            'deploy:prepare',
            'deploy:release',
            BuildTasks::TASK_UPLOAD_SHOP,
            BuildTasks::TASK_UPLOAD_CONFIG_DIR,
            BuildTasks::TASK_UPLOAD_PUB_STATIC_DIR,
            BuildTasks::TASK_UPLOAD_VAR_GENERATION_DIR,
            BuildTasks::TASK_FIX_FILE_OWNERSHIP,
            'deploy:shared', // link shared dirs / files
            MagentoTasks::TASK_SYMLINKS_ENABLE,
            'deploy:symlink', // ACTIVATE RELEASE
            MagentoTasks::TASK_MAINTENANCE_MODE_ENABLE,
            MagentoTasks::TASK_CACHE_DISABLE,
            MagentoTasks::TASK_SETUP_UPGRADE,
            MagentoTasks::TASK_CONFIG_DATA_IMPORT,
            MagentoTasks::TASK_CMS_DATA_IMPORT,
            MagentoTasks::TASK_CACHE_ENABLE,
            BuildTasks::TASK_FIX_FILE_OWNERSHIP,
            'deploy:clear_paths',
            MagentoTasks::TASK_MAINTENANCE_MODE_DISABLE,
            SystemTasks::TASK_PHP_FPM_RESTART,
            // SystemTasks::TASK_NGINX_RESTART,
            CleanupTasks::TASK_CLEANUP,
            'success',
        ]
    );
    
    after('deploy:prepare', BuildTasks::TASK_SHARED_DIRS_GENERATE);
    
    // Rollback in case of failure
    onFailure('deploy', DeployTasks::TASK_ROLLBACK);

    As you can see this file does not look like the default deploy.php files using lambda functions. We have moved the Task definition into a class N98\Deployer\Registry that is provided by n98/lib.n98.framework.deployer. Furthermore we have moved our tasks and their identifier to seperate classes to get them reusable and shareable using a composer package.
    Now let’s have a look at each section.

    deploy.php – configuration

    $sharedFiles = [
        'src/app/etc/env.php',
    ];
    set('shared_files', $sharedFiles);
    
    $sharedDirs = [
        'src/pub/media',
        'src/var/log',
        'src/var/session',
        'src/var/composer_home',
        'src/var/n98_integration',
    ];
    set('shared_dirs', $sharedDirs);
    
    set('writable_dirs', []);
    set('keep_releases', 6);
    set('ssh_type', 'native');
    set('bin/n98_magerun2', 'n98-magerun2');
    set('webserver-user', 'www-data');
    set('webserver-group', 'www-data');
    set('phpfpm_service', 'php7.0-fpm');
    set('nginx_service', 'nginx');

    We have added the default shared files and directories to the deployer default parameters shared_files and shared_dirs.
    ssh_type is set to native so we are using the ssh client provided by the operation system.
    webserver-user and webserver-group are used to apply the correct directory permissions.
    phpfpm_service and nginx_service is used to restart those services automatically during the deployment (using a custom task).

    deploy.php – servers

    $configLocal = __DIR__ . '/config/local.php';
    if (is_file($configLocal)) {
        require_once $configLocal;
    }
    require_once __DIR__ . '/config/staging.php';
    require_once __DIR__ . '/config/production.php';
    

    We have put the server specific configurations into separate files in the directory config. This way we can have a local.php.dist to setup a config for a local dev-environment.
    We could extend this to just include the environment provided as a parameter to deployer.

    A server config might look like this:

    <?php
    namespace Deployer;
    
    use N98\Deployer\RoleManager;
    
    $deployPath = '<PATH_ON_SERVER>';
    $sshConfigFile = '.ssh/config';
    
    $production = server('web01', '<IP>');
    $production->user('admin');
    $production->identityFile();
    $production->set('deploy_path', $deployPath);
    $production->stage('production');
    
    RoleManager::addServerToRoles('web01', ['web', 'db']);

    We are using the identityFile .ssh/config provided within the deploy repository. At first, I was assuming that deployer will use this file when running the native ssh commands and pass the config-file as a parameter like ssh -i .ssh/config . As it turns out it does not do that, instead it parses the ssh config-file and just extracts the Hostname, user and IdentifyFile directives.
    Though I will be creating a pull request that will make the usage of the config-file possible. I have tested it, and it works well, because why shouldn’t it.

    Futhermore we have created a class called RoleManager, which we use to define roles for servers and assign tasks to those roles. This functionality is needed for easily triggering specific tasks only on specific servers. It will be translated to $task->onlyOn() call later in the deployment. The main advantage and purpose is the ease of use and portability throught multiple deployment projects.

    deploy.php – adding the tasks

    To register our default Tasks we have created a Registry class that takes care of this process. This class also takes the roles mentioned above into account.

    Registry::register();

    With deployer you can define as much tasks as you like. It all comes together with your deploy pipeline that you define in your deploy.php.

    deploy.php – task classes

    We have split up all of our tasks to the following classes:

    • BuildTasks – tasks for basic initialization and an overwrite for the rollback
    • CleanupTasks – improved cleanup task
    • DeployTasks – improved rollback task
    • MagentoTasks – our Magento specific tasks
    • SystemTasks – tasks to restart nginx and php-fpm

    Those classes have class constants that are used to register the tasks and to define the build pipeline.

    I won’t go into to much detail regarding all the Tasks, because some of them are just triggering Magento commands. And it would just go beyond the scope of this post.
    If you are interested in more details about the Tasks just let me know, we might add another post highlighting and explaining them.

    Here is an excerpt from MagentoTasks:

    class MagentoTasks 
    {
        const TASK_SETUP_UPGRADE = 'magento:setup_upgrade';
    
        // …
    
        /**
         * Run Magento setup:upgrade
         */
        public static function runSetupUpgrade()
        {
            $srcDir = self::$srcDir;
            \Deployer\run("cd $srcDir; php bin/magento setup:upgrade --keep-generated");
        }
    
        // …
    }

    This is what the task action and the definition inside the Registry::register(); looks like this:

    self::registerTask(
        MagentoTasks::TASK_SETUP_UPGRADE, 'run Magento Updates',
        function () { MagentoTasks::runSetupUpgrade(); }, ['db']
    );

    With the Registry::registerTask being defined like this:

    protected static function registerTask($code, $desc, \Closure $body, array $roles = null)
    {
        \Deployer\desc($desc);
        $task = \Deployer\task($code, $body);
    
        if (is_array($roles)) {
            $servers = RoleManager::getServerListByRoles($roles);
    
            $task->onlyOn($servers);
        }
    
        return $task;
    }

    Using this method we are adding the default tasks to the deployer project and are applying the roles mentioned above.

    deploy.php – deploy pipeline

    Having defined all of our tasks, we can now take care of the deploy pipeline. This is how our default deploy pipeline for deployer is defined.

    desc('Deploy Project');
    task(
        'deploy', [
            'deploy:initialize',
            'deploy:prepare',
            'deploy:release',
            BuildTasks::TASK_UPLOAD_SHOP,
            BuildTasks::TASK_UPLOAD_CONFIG_DIR,
            BuildTasks::TASK_UPLOAD_PUB_STATIC_DIR,
            BuildTasks::TASK_UPLOAD_VAR_GENERATION_DIR,
            BuildTasks::TASK_FIX_FILE_OWNERSHIP,
            'deploy:shared', // link shared dirs / files
            MagentoTasks::TASK_SYMLINKS_ENABLE,
            'deploy:symlink', // ACTIVATE RELEASE
            MagentoTasks::TASK_MAINTENANCE_MODE_ENABLE,
            MagentoTasks::TASK_CACHE_DISABLE,
            MagentoTasks::TASK_SETUP_UPGRADE,
            MagentoTasks::TASK_CONFIG_DATA_IMPORT,
            MagentoTasks::TASK_CMS_DATA_IMPORT,
            MagentoTasks::TASK_CACHE_ENABLE,
            BuildTasks::TASK_FIX_FILE_OWNERSHIP,
            'deploy:clear_paths',
            MagentoTasks::TASK_MAINTENANCE_MODE_DISABLE,
            SystemTasks::TASK_PHP_FPM_RESTART,
            // SystemTasks::TASK_NGINX_RESTART,
            CleanupTasks::TASK_CLEANUP,
            'success',
        ]
    );
    
    after('deploy:prepare', BuildTasks::TASK_SHARED_DIRS_GENERATE);
    
    // Rollback in case of failure
    onFailure('deploy', DeployTasks::TASK_ROLLBACK);

    We have added the deploy:initialize task which will detect the stable release and save it with \Deployer\set(‘release_path_stable’, $releasePathStable);

    The BuildTasks::TASK_SHARED_DIRS_GENERATEwill ensure the necessary shared directories are available.

    The last thing I want to point out regarding the pipeline, is the rollback after an error during the deployment.

    onFailure('deploy', DeployTasks::TASK_ROLLBACK);

    By default deployer does not rollback in case somethings goes sideways. Deployer has a default task defined but it is not used by default, you would have to call it manually.

    Caveats

    While setting up this deployment pipeline we ran into different troubles with deployer. The rollback task and the detection of the current stable release are a bit buggy which is why we implemented an improved version ourselves. This improved version will not use an integer as the release directory but instead used the tag or branch being provided to deployer. The branch is getting postfixed with the current date and for the tags there is also a check to not deploy to the same directory twice.

    During development the releases folder might look something like this:

    develop-20170112142355
    develop-20170112142709
    develop-20170112143301
    develop-20170112145154
    develop-20170113073002

    Furthermore the standard cleanup tasks was also not quite stable and reliable, so we had to overwrite that too. We had situations where the former current release was deleted due to an issue how deployer builds its internal release_list. That error only occurred when multiple deploys went sideways.

    I am evaluating how much of our adjustments can be provided as a pull-request to the deployer project itself.

    Summary

    This is it, I hope you gathered some insights on how our deployment setup works and how you could setup your own.

    In the next blog post we will share some thought on where we want to go with this deployment in the future and how it is re-used in different environments and server setups.

    If you want to know or see more details, feel free to leave a comment or contact me directly on twitter, see the authors box below.

    See you next time.

    Teaser

    I am working on a default setup for a Magento2 deployment that can be used as starting point for deployment. Containing the most important tasks, the possibility to use for Pipeline builds, a default deployer setup, etc.

    So stay tuned 🙂

  • Deploying Magento2 – Jenkins Build-Pipeline [2/4]

    Deploying Magento2 – Jenkins Build-Pipeline [2/4]

    This post is part of series:

    Recap

    In the post Deploying Magento2 & History / Overview [1/4] we showed an overview of our deployment for Magento2 and this post will go into more detail on what is happing on the Build-Server and how it is done. So to get you up to speed, this is the overview of our process and what this post will cover:

    Jenkins Build-Pipeline

    Our Build Server is basically a Jenkins running on a dedicated server. The Jenkins Server is the main actor in the whole deployment process.
    It will control the specific phases of the deployment and provide an overview and a detailed monitoring of the output of each phase.

    We are using the Jenkins Build Pipeline feature to organize and control our deployment.
    The Magento2 deployment is split up into the following stages:

    • Tool Setup – ensuring all tools are installed
    • Magento Setup – updating the source-code and update composer dependencies
    • Asset Generation – generating the assets in pub/static var/di var/generation and providing them as packages
    • Deployment – delivering the new release to the production server

    The Jenkinsfile

    There are different ways to create a Jenkins Build-Pipeline, one is to create a Jenkinsfile that defines the stages and the commands to run. We are using just that approach and put that Jenkinsfile into a git repository separate from our magento2 repository. Though this is an approach we have been following for years now, I still think it is best to have your deployment separate from the actual project. But as so often that depends on the individual needs.
    We will add some more dependencies to this repository later.

    Next you will see a skeleton for the Jenkinsfile we are using. I left out the details for the stages for now and will show those further down the post.

    node {
        // ENV variables
        env.PWD = pwd()
        env.STAGE = STAGE
        env.TAG = TAG
        env.REINSTALL_PROJECT = REINSTALL_PROJECT
        env.DELETE_VENDOR = DELETE_VENDOR
        env.GENERATE_ASSETS = GENERATE_ASSETS
        env.DEPLOY = DEPLOY
    
        try {
            // Update Deployment
            checkout scm
    
            stage 'Tool Setup'
            // Setup tools here
    
            stage 'Magento Setup'
            // Setup and update Magento
            
            stage 'Asset Generation'
            if (GENERATE_ASSETS == 'true') {
                // Generate and package assets
            }
    
            stage 'Deployment'
            if (DEPLOY == 'true') {
                // Trigger deployment and start release
            }
    
        } catch (err) {
            currentBuild.result = 'FAILURE'
            throw err
        }
    }
    

    The stage keyword defines a new stage and takes a string as a parameter. You can see the stages I mentioned earlier defined here. The update of our deployment itself is not included as a stage.
    We are using multiple ENV variables that are defined when starting the build. By default DEPLOY and GENERATE_ASSETS are set to true , but we could choose to leave out on of them. So in case there was an error during the Deployment we don’t need to re-generate all the assets.
    The ENV variables REINSTALL_PROJECT and DELETE_VENDOR are used within the stage Magento Setup.

    The ENV variable STAGE is used to identify the server environment we are deploying to, like staging or production. This variable is to be selected when starting the Build and can be individualized to the needs in the project at hand.
    The ENV variable TAG is defining the git branch or git tag where are deploying with this build. It is used later on in the process multiple times.

    Stage Tool Setup

    stage 'Tool Setup'
    sh "${phpBin} -v"
    // Composer deps like deployer
    sh "composer.phar install"
    // Phing
    if (!fileExists('phing-latest.phar')) {
        sh "curl -sS -O https://www.phing.info/get/phing-latest.phar -o ${phingBin}"
    }
    sh "${phingCall} -v"
    sh "printenv"
    

    The first stage “Tool Setup” will install or update the tools needed through out the deployment.
    As you can see we are using composer here to pull in our tools like for example deployer.
    Also we are using phing for some parts during the deployment process, so we are ensuring that the latest phing version is present.

    Stage Magento Setup

    stage 'Magento Setup'
    if (!fileExists('shop')) {
        sh "git clone ${magentoGitUrl} shop"
    } else {
        dir('shop') {
            sh "git fetch origin"
            sh "git checkout -f ${TAG}"
            sh "git reset --hard origin/${TAG}"
        }
    }
    dir('shop') {
        sh "${phingCall} jenkins:flush-all"
        sh "${phingCall} jenkins:setup-project"
        sh "${phingCall} jenkins:flush-all"
    }

    In this stage we are updating the Magento Setup the Build needs to create the assests.
    It basically consists of two steps:

    • Setup or Update the Source-Code of the Magento Shop
    • Setup or Update the Magento-Database

    We are cloning the repository containing the customer project in the directory shop. If we have already cloned the repository we will just update to the tag or branch that is to be deployed.

    Next-up is the project setup using the phing-call jenkins:setup-project. This phing-call is defined by the phing scripts inside our shop repository.
    This call will

    • install the magento composer dependencies,
    • re-install the project therefore deleting the app/etc/env.php, (using REINSTALL_PROJECT )
    • create the database if necessary
    • run setup:upgrade

    Up until recently a database was necessary to create the assests. As far as I know, there is plan to remove the requirement of having a database during the assets creation.

    The phing tasks called in this stage are re-used from our Continous Build Jobs that we run on develop, master, feature and release branches for all of our projects.
    Those Build Jobs are automatically running the Unit and Integration Tests, generating the documentation, Running Code Analyzers and summarizing all this information in a nice little Dashboard.
    Maybe we will have a blog-post about that too. Let’s move on to the next stage.

    Stage Asset Generation

    stage 'Asset Generation'
    dir('shop') {
        if (GENERATE_ASSETS == 'true') {
            sh "${phingCall} deploy:switch-to-production-mode"
            sh "${phingCall} deploy:compile"
            sh "${phingCall} deploy:static-content"
            sh "bash bin/build_artifacts_compress.sh"
    
            archiveArtifacts 'config.tar.gz'
            archiveArtifacts 'var_di.tar.gz'
            archiveArtifacts 'var_generation.tar.gz'
            archiveArtifacts 'pub_static.tar.gz'
            archiveArtifacts 'shop.tar.gz'
        }
    }

    During this stage the deploy job will compile all assets needed for running Magento2 in production-mode.
    Therefore we ensure we are in production-mode and basically call php bin/magento setup:di:compile  and php bin/magento setup:static-content:deploy .
    Those phing-calls you see above are executing the following commands:

    php bin/magento deploy:mode:set --skip-compilation production
    
    rm -Rf var/di
    rm -Rf var/generation
    
    php bin/magento setup:di:compile
    
    bin/magento setup:static-content:deploy --theme=NAMESPACE/base --theme=Magento/backend --language=en_US --language=de_DE
    

    The Bash-Script bin/build_artifacts_compress.sh  creates 5 tar files for

    • shop – containing the Magento Source-Code
    • pub_static – containing the contents of pub/static directory
    • var_generation – containing the contents of var/generation directory
    • var_di – containing the contents of var/di directory
    • config – containing config yaml-files that can be imported using config:data:import

    The config:data:import  command is provided by the Semaio_ConfigImportExport which we are using to manage our systems configuration through.  https://github.com/semaio/Magento2-ConfigImportExport
    After the artifacts have been created, we use the Jenkins archiveArtifacts command to archive the latest artifacts for this build and make them available per HTTP-link in a consistent directory.

    At the moment we are thinking about just creating one artifact instead of 5 and using that from here on. This will have some more advantages that we will cover in our post: “Future Prospect (cloud deployment, artifacts)”

    Now we have prepared all the artifacts we need and are ready to create the new release on our servers and publish it. So now for the final stage “Deployment”.

    Stage Deployment

    stage 'Deployment'
    if (DEPLOY == 'true') {
        sshagent (credentials: [jenkinsSshCredentialId]) {
            sh "./dep deploy --tag=${TAG} ${STAGE}"
        }
    }

    This Stage has probably the shortest content as far as the code in the Jenkinsfile is concerned. We are just triggering the Deployer while passing the STAGE and the TAG to it.

    Deployer is a Deployment Tool for php and is more or less based upon capistrano and following the same concepts applied in capistrano.

    We have defined quite some Magento2 related Deployer Tasks and created some adjustments to the core-tasks fixing bugs or adjusting them to our needs.

    The details what we have done and on how we are using deployer to release the code and pushing the assets to the server environment will be covered in the upcoming post.

    The Stage View of the Pipeline

    At this point we have defined the Build-Pipeline and are ready to execute it.
    We do so by configuring the parameters as needed in this form:

    You can see the Environment Variables used in the above mentioned code samples. The image shows the default form with pre-selected variables.
    In some cases it is necessary to delete the vendor directory completely or to drop the jenkins database.

    When running the introduced Build-Pipeline, you are presented with an informative stage view that shows the stages and their completion.
    We can evaluate how our Deployment is progressing and get an estimate how long it will take to finish the stage(s).

    The Jenkins Job Configuration

    We are creating our projects based on the pipeline project.
    Then the parameters are added and the git repository url and that’s basically it.

    Here are some screenshots:

    Repository Configuration
    Build Job Parameters

     

    Summary

    This is the end of the introduction to our Build-Pipeline Setup for Deployments. The next post will cover details to our php-deployer setup.

    I really like the automated and centralized way of Deploying our Magento Shops and of course the resulting advantages. Whenever somethings automated you don’t need to explicitly know or remember all the details of the deployment. It just takes so much of your mind and you can focus on more important tasks.

    Well, that’s it for this post. I hope you enjoyed it and you find it informative. As always, if there any questions or if you’d like to know more about specific details, please feel free to comment or ask us directly on twitter or any other social plattform.

    UPDATE 23-FEB-2017

    Add Screenshot of the Build Form.

    UPDATE 29-MAR-2017

    Add Screenshot of the Build Configuration Parameters and Pipeline

  • Deploying Magento2 – History & Overview [1/4]

    Quite recently we have updated the deployment of our Magento2 projects to a more flexible and reusable way.
    Originally I wanted to create one post to present you our deployment setup, the systems involved, the workflow & process and some code that might be interesting.
    While describing this subject I decided to create a series of posts to cover those parts as it was just getting to extensive for one post.

    I am planing to cover the following topics in separate posts:

     

    Then let’s get started with a brief introduction of our former setup for Magento1 and Magento2. You might have a similar solution to this one.

    History Magento 1

    When we were starting with our first Magento 2 project in June 2016, we ported the workflow we had established for our Magento 1 projects.

    Our Magento 1 projects are deployed using capistrano and are using a pull approach, where the server fetches the Magento source-code and the composer dependencies using git and composer.
    This approach has some draw backs as we have to have tools installed (git, composer) and we needed a transfer channel back to our gitlab server.

    First Magento 2 Deployment

    At first, we followed the same approach with the Magento 2 projects.
    We applied some minor adjustments to the process, as Magento 2 has some requirement in terms of pre-compilation.


    The solution was to generate the static assets and the di on the build server and pushing that to the production server during the deployment.
    You should not generate those assets on the production server. The actions performed on the production server should be kept to a minimum, to keep the load down and when thinking about a setup with multiple nodes it just does not seem right to this kind of task on each server.
    We ended up with a mixture of a PULL and PUSH deployment, the source-code being pulled by git / composer and the assets being pushed.
    We still had the drawbacks from our Magento 1 deployment, as we basically just extended that to fit the needs of the Magento 2 compilation.

    Current Magento 2 Deployment

    Our goal was to create a pure PUSH deployment where the production server does not need direct access to our git repositories.

    So here is an Overview of how our current deployment for Magento 2 projects works:

    Jenkins will fetch the source-code using git and composer and update its Magento 2 instance database. It will then generate & package the assets and finally pushes the code and the assests to production server.
    To achieve those steps we have setup a Jenkins Pipeline that we will have a look at next.

    Summary

    This setup comes in handy when any callback (e.g. PULL) is prohibited by a firewall.
    Or even a more restrictive environment where a VPN-Tunnel is to be opened which prevents any other network connections to other system except the server.
    We were faced with that kind of situations recently, but with using the above mentioned approach we had little effort in terms of adjusting our deployment.

    Furthermore following this approach we are flexible where we push to and it is easy to extend and reusable.
    In the post “Future Prospect (cloud deployment, artifacts)” we will shed some light on our future plans on how to extend that deployment for different hosting environments.

    This was a brief overview of how we got to our current Magento2 Deployment and how it works in general.
    The next posts will be about the tool-stack we use and we will share some insights with code samples.

    If you have any feedback or questions, as always, feel free to leave a comment or contact us directly.

  • How to Create a Category Image Attribute in Magento 2

    Creating a custom Category Attribute with an image upload is quite common feature requirement in our shops.
    So this blog-post will be about the steps you have to take to create a custom category attribute with an image upload in Magento 2.
    The post turned out to be quite long, but I wanted to provide a complete description of all steps necessary. So stay with us if your interested 🙂

    Our Module is called `Dev98_CategoryAttributes` and the image attribute will be called `dev98_icon`.

    Creating the Attribute

    In our projects we are creating the attributes using Install- or UpdateData classes, except when the attributes are explicitly managed manually.
    So to create the`dev98_icon` we might write something like this.

    $this->eavSetup->addAttribute(
        CategoryModel::ENTITY,
        'dev98_icon',
        [
            'type' => 'varchar',
            'label' => 'dev98 Icon',
            'input' => 'image',
            'sort_order' => 333,
            'source' => '',
            'global' => 2,
            'visible' => true,
            'required' => false,
            'user_defined' => false,
            'default' => null,
        ]
    );

    After running the setup:upgrade command to upgrade the application database and schema, we have created the attribute and should be able to store information for this attribute.

    Extending the Category Form

    To get an file upload for our attribute we need to extend the ui-component for the category form.
    The category form is defined in a category_form.xml, which can be extend by adding the following code to the file
    `Dev98/CategoryAttributes/view/adminhtml/ui_component/category_form.xml`

    <?xml version="1.0" ?>
    <!--
    /**
     * @copyright Copyright (c) 1999-2016 netz98 new media GmbH (http://www.netz98.de)
     *
     * @see PROJECT_LICENSE.txt
     */
    -->
    <form xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
          xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Ui:etc/ui_configuration.xsd">
        <fieldset name="dev98_attributes">
            <argument name="data" xsi:type="array">
                <item name="config" xsi:type="array">
                    <item name="collapsible" xsi:type="boolean">true</item>
                    <item name="label" xsi:type="string" translate="true">dev98 custom attributes</item>
                    <item name="sortOrder" xsi:type="number">100</item>
                </item>
            </argument>
            <field name="dev98_icon">
                <argument name="data" xsi:type="array">
                    <item name="config" xsi:type="array">
                        <item name="dataType" xsi:type="string">string</item>
                        <item name="source" xsi:type="string">category</item>
                        <item name="label" xsi:type="string" translate="true">dev98 icon</item>
                        <item name="visible" xsi:type="boolean">true</item>
                        <item name="formElement" xsi:type="string">fileUploader</item>
                        <item name="elementTmpl" xsi:type="string">ui/form/element/uploader/uploader</item>
                        <item name="previewTmpl" xsi:type="string">Magento_Catalog/image-preview</item>
                        <item name="required" xsi:type="boolean">false</item>
                        <item name="uploaderConfig" xsi:type="array">
                            <item name="url" xsi:type="url"
                                  path="dev98_category_attributes/category/categoryImageUpload/attribute_code/dev98_icon"/>
                        </item>
                        <item name="scopeLabel" xsi:type="string">[WEBSITE]</item>
                    </item>
                </argument>
            </field>
        </fieldset>
    </form>

    The above code block defines a new fieldset `dev98_attributes` which will contain one field`dev98_icon`.
    There is one parameter for the field `dev98_icon` I want to point out, which is the`uploaderConfig` array.
    In this array we have a parameter called url which defines the ActionController which is responsible for handling the image upload.
    As you can see we have provided a custom ActionController for handling the upload, because after taking a closer look to the Controller Magento uses for it’s category image upload, you will see why.
    The file is `\Magento\Catalog\Controller\Adminhtml\Category\Image\Upload`

    class Upload extends \Magento\Backend\App\Action
    {
        // […]
    
        /**
         * Upload file controller action
         *
         * @return \Magento\Framework\Controller\ResultInterface
         */
        public function execute()
        {
            try {
                $result = $this->imageUploader->saveFileToTmpDir('image');
    
                $result['cookie'] = [
                    'name' => $this->_getSession()->getName(),
                    'value' => $this->_getSession()->getSessionId(),
                    'lifetime' => $this->_getSession()->getCookieLifetime(),
                    'path' => $this->_getSession()->getCookiePath(),
                    'domain' => $this->_getSession()->getCookieDomain(),
                ];
            } catch (\Exception $e) {
                $result = ['error' => $e->getMessage(), 'errorcode' => $e->getCode()];
            }
            return $this->resultFactory->create(ResultFactory::TYPE_JSON)->setData($result);
        }
    }

    The Magento ActionController for handling the image upload is hardwired against the attribute `image`. There is no reasonable way we can reuse this code, so we will create our own in the next step.

    Image Upload ActionController

    Basically we have copied the code from Magento and refactored it to be more extensible:

    namespace Dev98\CategoryAttributes\Controller\Adminhtml\Category;
    
    use Magento\Framework\Controller\ResultFactory;
    
    class CategoryImageUpload extends \Magento\Backend\App\Action
    {
        // […]
    
        /**
         * Upload file controller action
         *
         * @return \Magento\Framework\Controller\ResultInterface
         */
        public function execute()
        {
            try {
                $attributeCode = $this->getRequest()->getParam('attribute_code');
                if (!$attributeCode) {
                    throw new \Exception('attribute_code missing');
                }
    
                $basePath = 'catalog/category/dev98/' . $attributeCode;
                $baseTmpPath = 'catalog/category/dev98/tmp/' . $attributeCode;
    
                $this->imageUploader->setBasePath($basePath);
                $this->imageUploader->setBaseTmpPath($baseTmpPath);
    
                $result = $this->imageUploader->saveFileToTmpDir($attributeCode);
    
                $result['cookie'] = [
                    'name' => $this->_getSession()->getName(),
                    'value' => $this->_getSession()->getSessionId(),
                    'lifetime' => $this->_getSession()->getCookieLifetime(),
                    'path' => $this->_getSession()->getCookiePath(),
                    'domain' => $this->_getSession()->getCookieDomain(),
                ];
            } catch (\Exception $e) {
                $result = ['error' => $e->getMessage(), 'errorcode' => $e->getCode()];
            }
    
            return $this->resultFactory->create(ResultFactory::TYPE_JSON)->setData($result);
        }
    }
    

    I left out the constructor and the `_isAllowed` method for a better readability.

    Our ActionController expects one argument `attribute_code` and hands that to the `imageUploader` which will handle the upload of the image.
    Now we have a reusable ActionController if we have to provide more custom category images.
    And as it turned out in one of our projects, we did need more than one.
    Furthermore we might extract that code into a vendor module.

    Current Status

    √ Create the Attribute
    √ Create category_form field
    √ Create the Controller handling the upload

    We have already done quite some work but we are not done yet.

    If you try running the code created so far you might see an Image Upload and the Image will be uploaded to the tmp folder `media/catalog/category/dev98/tmp/`.
    But it will remain there and there will be nothing saved to our `dev98_icon` attribute for this category.

    So why is that?

    The data for our `dev98_icon` field in the request is provided as an array. And as there is nothing changed regarding that fact, we end up with nothing being saved to our attribute.
    Futhermore, we need to handle the move from the tmp directory to the final image destination ourselves.
    That is OK in my opinion as there might be an error during the category save and we only want the image to be moved to its final destination when the save was successful.

    Category Save Observers

    To do so we need to observe the following two events:

    • catalog_category_prepare_save
    • catalog_category_save_after

    In our Observer for `catalog_category_prepare_save` we will convert the attribute image array to a string and in the `catalog_category_save_after` we will move the image from the tmp folder to its destination.

    <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:Event/etc/events.xsd">
        <event name="catalog_category_prepare_save">
            <observer name="dev98_category_attributes_prepare_image_save"
                      instance="Dev98\CategoryAttributes\Observer\Category\CategoryImageDataPrepare" />
        </event>
    
        <event name="catalog_category_save_after">
            <observer name="dev98_category_attributes_save_after"
                      instance="Dev98\CategoryAttributes\Observer\Category\CategoryAttributesAfterSave" />
        </event>
    </config>

    `Dev98\CategoryAttributes\Observer\Category\CategoryImageDataPrepare`

    namespace Dev98\CategoryAttributes\Observer\Category;
    
    use Magento\Framework\Event\Observer;
    use Magento\Framework\Event\ObserverInterface;
    
    /**
     * Class ImageDataPrepare
     */
    class CategoryImageDataPrepare implements ObserverInterface
    {
        /**
         * List of available cms page image attributes
         *
         * @var []
         */
        protected $imageAttributes = [
            'dev98_icon',
        ];
    
        /**
         * Prepare image data before save
         *
         * @param Observer $observer
         *
         * @return $this
         */
        public function execute(Observer $observer)
        {
            /** @var \Magento\Catalog\Model\Category $category */
            $category = $observer->getCategory();
            $data = $observer->getRequest()->getParams();
            foreach ($this->imageAttributes as $attributeName) {
                if (isset($data[$attributeName]) && is_array($data[$attributeName])) {
                    if (!empty($data[$attributeName]['delete'])) {
                        $data[$attributeName] = null;
                    } else {
                        if (isset($data[$attributeName][0]['tmp_name'])) {
                            $data['is_uploaded'][$attributeName] = true;
                        }
                        if (isset($data[$attributeName][0]['name']) && isset($data[$attributeName][0]['tmp_name'])) {
                            $data[$attributeName] = $data[$attributeName][0]['name'];
                        } else {
                            unset($data[$attributeName]);
                        }
                    }
                }
                if (isset($data[$attributeName])) {
                    $category->setData($attributeName, $data[$attributeName]);
                }
            }
            if (isset($data['is_uploaded'])) {
                $category->setData('is_uploaded', $data['is_uploaded']);
            }
    
            return $this;
        }
    }

    The execute method looks rather complicated, but what it basically does is to set the image-name to the category model and mark the category as  `is_uploaded` so later-on we can detect we have to move an image. This code is mostly copied from the Magento Core, because as it turned out this part is also not reusable – yet.

    Current Status 2

    √ Create the Attribute
    √ Create category_form field
    √ Create the Controller handling the upload
    √ Observe the CategoryPrepareSave to get image name saved
    √ Observe the CategoryAfterSave to get image moved to destination

    But now we are done you might think.

    Well. No. There are still further steps to take to get this working completely.

    At this moment your image upload and saving will be working, but after refreshing the page you will see there is still something wrong as the image is either not shown at all or a placeholder image is shown.

    Plugin Category DataProvider

    After some debugging you will find out that there is class called  `\Magento\Catalog\Model\Category\DataProvider` which is used to provide the category data for the form.
    In this class we will find a method `getData` which does the image preparation for the standard `image`, again in a non extensible way:

    /**
     * Get data
     *
     * @return array
     */
    public function getData()
    {
        if (isset($this->loadedData)) {
            return $this->loadedData;
        }
        $category = $this->getCurrentCategory();
        if ($category) {
            $categoryData = $category->getData();
            $categoryData = $this->addUseDefaultSettings($category, $categoryData);
            $categoryData = $this->addUseConfigSettings($categoryData);
            $categoryData = $this->filterFields($categoryData);
            if (isset($categoryData['image'])) {
                unset($categoryData['image']);
                $categoryData['image'][0]['name'] = $category->getData('image');
                $categoryData['image'][0]['url'] = $category->getImageUrl();
            }
            $this->loadedData[$category->getId()] = $categoryData;
        }
        return $this->loadedData;
    }

    Next up we will create a Plugin that integrates after the  `getData` method and does the preparation for our custom attributes.

    Let’s create the `di.xml` with the Plugin class called
    `Dev98\CategoryAttributes\Plugin\Category\CategoryDataProviderPlugin`

    <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd">
        <type name="Magento\Catalog\Model\Category\DataProvider">
            <plugin name="dev98_categoryattributes_dataprovider_plugin"
                    type="Dev98\CategoryAttributes\Plugin\Category\CategoryDataProviderPlugin" />
        </type>
    </config>

    And finally our Plugin class itself:

    <?php
    /**
     * @copyright Copyright (c) 1999-2016 netz98 GmbH (http://www.netz98.de)
     *
     * @see PROJECT_LICENSE.txt
     */
    
    namespace Dev98\CategoryAttributes\Plugin\Category;
    
    use Dev98\CategoryAttributes\Api\CategoryUrlRepositoryInterface;
    use Magento\Catalog\Model\Category;
    use Magento\Catalog\Model\Category\DataProvider as CategoryDataProvider;
    
    /**
     * CategoryDataProviderPlugin
     */
    class CategoryDataProviderPlugin
    {
        /**
         * @var CategoryUrlRepositoryInterface
         */
        private $categoryUrlRepository;
    
        /**
         * CategoryDataProviderPlugin constructor.
         *
         * @param CategoryUrlRepositoryInterface $categoryUrlRepository
         *
         * @internal param StoreManagerInterface $storeManager
         */
        public function __construct(CategoryUrlRepositoryInterface $categoryUrlRepository)
        {
            $this->categoryUrlRepository = $categoryUrlRepository;
        }
    
        /**
         * AfterGetData
         *
         * we need to modify the data Array returned and generate the data array.
         * This includes the correct image url.
         * Without the information the Admin will not show this field due to an error
         *
         * @param CategoryDataProvider $subject
         * @param array $data
         *
         * @return array
         * @SuppressWarnings(PHPMD.UnusedFormalParameter)
         */
        public function afterGetData(CategoryDataProvider $subject, array $data)
        {
            /** @var Category $category */
            $category = $subject->getCurrentCategory();
            if (!$category) {
                return $data;
            }
    
            $attributeCodes = [
                'dev98_icon',
            ];
    
            foreach ($attributeCodes as $attributeCode) {
                $image = $category->getData($attributeCode);
                if (!$image) {
                    continue;
                }
    
                $imageName = $image;
                if (!is_string($image)) {
                    if (is_array($image)) {
                        $imageName = $image[0]['name'];
                    }
                }
                $categoryImageUrl = $this->categoryUrlRepository->getCategoryIconUrl($category, $attributeCode);
                $seoImageData = [
                    0 => [
                        'name' => $imageName,
                        'url' => $categoryImageUrl,
                    ],
                ];
                $data[$category->getId()][$attributeCode] = $seoImageData;
            }
    
            return $data;
        }
    }

    As you can see we extracted the url generation of the icon to a `CategoryUrlRepository` as this funcationality is need within more classes and in other modules as well.

    So last but not least here is the CategoryUrlRepository:

    <?php
    /**
     * @copyright Copyright (c) 1999-2016 netz98 GmbH (http://www.netz98.de)
     *
     * @see PROJECT_LICENSE.txt
     */
    
    namespace Dev98\CategoryAttributes\Repository;
    
    use Dev98\CategoryAttributes\Api\CategoryUrlRepositoryInterface;
    use Magento\Catalog\Api\Data\CategoryInterface;
    use Magento\Framework\Api\AttributeInterface;
    use Magento\Framework\UrlInterface;
    use Magento\Store\Api\Data\StoreInterface;
    use Magento\Store\Model\StoreManagerInterface;
    
    /**
     * CategoryUrlRepository
     */
    class CategoryUrlRepository implements CategoryUrlRepositoryInterface
    {
        /**
         * @var StoreManagerInterface
         */
        private $storeManager;
    
        /**
         * CategoryUrlRepository constructor.
         *
         * @param StoreManagerInterface $storeManager
         */
        public function __construct(StoreManagerInterface $storeManager)
        {
            $this->storeManager = $storeManager;
        }
    
        /**
         * @param CategoryInterface $category
         * @param $attributeCode
         *
         * @return string
         */
        public function getCategoryIconUrl(CategoryInterface $category, $attributeCode)
        {
            $url = '';
    
            $imageAttribute = $category->getCustomAttribute($attributeCode);
    
            if (!$imageAttribute instanceof AttributeInterface) {
                return $url;
            }
    
            $imageName = $imageAttribute->getValue();
    
            if (!$imageName) {
                return $url;
            }
    
            /** @var StoreInterface $store */
            $store = $this->storeManager->getStore();
            $baseUrl = $store->getBaseUrl(UrlInterface::URL_TYPE_MEDIA);
            $url = $baseUrl . 'catalog/category/dev98/' . $attributeCode . '/' . $imageName;
    
            return $url;
        }
    
    }
    

    That was the final class we needed to create to get this image upload working through out.

    Full Checklist

    Here is a short Summary of the classes and files created.

    √ Create the Attribute
    √ Create category_form field
    √ Create the Controller handling the upload
    √ Observe the CategoryPrepareSave to get image name saved
    √ Observe the CategoryAfterSave to get image moved to destination
    √ Create Plugin for Category\DataProvider to prepare Image Data for Frontend

    Final Thoughts

    If you have read this far, thanks for bearing with us on this one.
    We really spend some time debugging the different issues we had while creating the image upload.
    Multiple times we weren’t quite sure whether the error was in the javascript part or server-side.
    Sometimes we had to even debug javascript code to find out what was wrong with the implementation on the server-side, as there where errors that were not shown or the data was not provided as needed.

    But it was worth the effort and we know have a good glimpse of how the Category Admin is implemented.

    The code is still not perfect and might need some further polishing, but it should give you a good starting point.
    Right now I am thinking about putting together a public module on github to share this implementation.

    If you are interested in the full code of the module please let me know.
    And if there is an easier way to achieve these please let me know as well.

     

  • How to create attribute-options in Magento2

    Recently we had to create Configurable Products in our Product Import.
    To create those products we had to make sure that all simple products are generated before the configurable products and that all the attribute options for our configurable attribute are available.
    So this post will focus on how to create those attribute options and a problem where we had to spend some time figuring out.

    Preface

    We need to create those attribute options before we call
    $this->magentoImporter->validateSource($dataSource);
    otherwise the validation will fail because the options are validated as well.

    So let’s assume we have a collection of attribute-options that we want to ensure for an attribute.

    $attributes = [
        'dev98_diameter' => [
            '20mm' => '20mm'
            '30mm' => '30mm'
        ]
    ];

    Creating the Options

    We will iterate this array and generate an Option Data-Object for each of the attribute-options.
    This will be done using the Service Contract from Magento_Eav with the following classes:

    • Magento\Eav\Api\AttributeOptionManagementInterface (used to create the option)
    • Magento\Eav\Api\Data\AttributeOptionInterface  (one option)
    • Magento\Eav\Api\Data\AttributeOptionInterfaceFactory  (used as $this->optionFactory, to create an Option Data-Object)

    The resulting code might look something like this.

    foreach (attributes as $attributeCode => $options) {
        foreach ($options as $optionValue => $optionKey) {
            /** @var AttributeOptionInterface $option */
            $option = $this->optionFactory->create();
    
            $option->setLabel($optionValue);
    
            // Add Option
            $this->optionManagement->add(Product::ENTITY, $attributeCode, $option);
        }
    }

    After running this peace of code you will get attribute-options for your attribute.
    Note: This code does not check whether an attribute-option is already in the database.

    Running the Import

    Now when you start you product import using the Magento Core functionality your configurable products will be created and the simple items will be assigned properly.

    What I left out until now, was the trouble we had finding out how to create the option correctly.
    So the next chapter will be about the issue we face, how we solved it and based on that what not to do when creating an option.

    Problem with generating the Options

    At first we tried generating our options like this:

    $option = $this->optionFactory->create();
    $option->setValue($optionValue);
    $option->setLabel($optionValue);
    
    // Add Option
    $this->optionManagement->add(Product::ENTITY, $attributeCode, $option);

    As it turns this will fail with an Foreign-Key Constraint Violation while inserting the Option,
    basically saying the referenced option_id cannot be found.

    After some intense debugging I found out what was going on.
    Inside the OptionManagement the option->getValue()  is used to define the option_id if it is set.
    \Magento\Eav\Model\Entity\Attribute\OptionManagement::getOptionId

    private function getOptionId($option)
    {
         return $option->getValue() ?: 'new_option';
    }

    Debugging the Generation of an Option further we end up in the following method
    \Magento\Eav\Model\ResourceModel\Entity\Attribute::_updateAttributeOption

    protected function _updateAttributeOption($object, $optionId, $option)
    {
        $connection = $this->getConnection();
        $table = $this->getTable('eav_attribute_option');
        $intOptionId = (int)$optionId;
        
        // …
    
        if (!$intOptionId) {
            $data = ['attribute_id' => $object->getId(), 'sort_order' => $sortOrder];
            $connection->insert($table, $data);
            $intOptionId = $connection->lastInsertId($table);
        } else {
            $data = ['sort_order' => $sortOrder];
            $where = ['option_id = ?' => $intOptionId];
            $connection->update($table, $data, $where);
        }
    }

    This method gets the before mentioned option_id aka option->getValue()  injected, does an int-cast and based on that decides whether it is an update or insert.

    So if we come through this method with our sample having $optionId set to “20mm”
    the int-cast will return 20 and assume that the option is available and an update is to be made.
    That is the main reason for the mySQL Foreign-Key Exception.

    Why does it work with ‘new_option’?

    In case we leave out the $option->setValue()  in our implementation
    the optionId will be set to ‘new_option’ as shown above.

    The int-cast of the string ‘new_option’ will be 0 and therefore it will be detected as an insert.

    Conclusion

    It would have been great if Magento had some kind of validation as a first instance within the OptionManagement implementation, like throwing an Exception when the option->getValue()  is not an int. Or validating that the provided option_id is valid.

    Finally I want to point out, that you should NOT call $option->setValue(…)unless you have the exact option_id for this Option available.

    I hope this post is helpful and will save you some trouble when generating custom options programmatically.

    If you had similar experience or have something to add, feel free to leave a comment. Your feedback is always welcome.

  • How to adjust increment-ids in Magento 2

    Maybe almost every Magento developer has had the task to customize the increment-ids for orders or customers in Magento.

    Recap Magento 1

    In Magento 1 you had to change the column increment_prefix  in the table eav_entity_store .
    I am sure there are modules out there that let you achieve that in convenient way.
    We have done that by using Setup-scripts most of the time.
    When a new store is created you need set the increment_prefix afterwards and do so for all entities that you need and that are defined in eav_entity_type.
    So far an rather straight forward task I would say.

    What has changed in Magento 2

    Let’s take a look at the eav_entity_store  table.

    In this database an order and a rma has already been created.
    But as you can see the order and customer entity no longer is maintained within this table.

    In Magento 2 the increment-id generation mechanism basically has not changed for certain entities. The increment-id generation for customers and RMAs still uses the eav_entity_store .

    The sales module has received some changes regarding the generation of those increment-ids.

    Let’s have a look at what changed in Magento2 regarding the order increment-id generation.
    There are two new tables that you may not have seen yet, them being sales_sequence_meta  and sales_sequence_profile .
    In the example below you see the content of those for the default website and store setup.

    Table sales_sequence_meta

    There is a table called sales_sequence_meta  which defines store-specific sequence-tables for the sales entity-types.

    Table sales_sequence_profile

    Then we have a table sales_sequence_profile  that contains the configuration for the increment-id generation.

    Most of the columns are pretty self-explanatory.
    You can define a prefix  and suffix  that is added to the beginning respectively the ending of the generated increment-id.
    With the start_value  you can define the first increment-id.
    The step  column defines the increments that are made between increment-ids.

    So to change the prefix for the order increment-ids we need to change to column prefix to the desired value, “DEVMWR” in this case.
    This will result in an order increment-id like DEVMWR000000004

    After that change the orders will receive a custom increment-id.

    If you have any notes feel free to leave a comment.