Tag: di

  • Use EavSetup to Import Attributes

    I recently had the issue that I needed to use the \Magento\Eav\Setup\EavSetup outside the setup-context.
    To be a bit more concrete, I wanted to import attribute-sets, attributes and attribute-options without using an install-script.
    My first idea was

    in magento2 you can easily inject the ‘EavSetup’ via constructor injection and then use it in your own class

    First try

    So I injected into my class which worked out well in the development-mode.

    /**
     * SomeAttribute constructor.
     *
     * @param \Some\Own\AttributeContext $context
     * @param \Magento\Eav\Setup\EavSetup $eavSetup
     * @param string $entityTypeId
     *
     */
    public function __construct(
        \Some\Own\AttributeContext $context,
        \Magento\Eav\Setup\EavSetup $eavSetup,
        $entityTypeId = Product::ENTITY
    ) {
        parent::__construct($context);
    
        $this->eavSetup = $eavSetup;
        $this->entityTypeId = $entityTypeId;
    }

    But using the same code with the production-mode a fatal error occurred with a message like

    ERROR: PHP Fatal error:  Uncaught TypeError: Argument 1 passed to Magento\Setup\Module\DataSetup::__construct() must be an instance of Magento\Framework\Module\Setup\Context, 
    instance of Magento\Framework\ObjectManager\ObjectManager given, 
    called in [...]/src/vendor/magento/framework/ObjectManager/Factory/AbstractFactory.php on line 93 and defined in [...]/src/setup/src/Magento/Setup/Module/DataSetup.php:57
    

    After a short research and some debugging, it turned out that the “production”-mode was not the issue. The root of the problem was the output “setup:di:compile”-command created. And more over how the EavSetup is instantiated in general.

    A brief digression

    For better testing I wrote a short php-file which tries to instantiate my class with the help of the object-manager. It is based on the ‘index.php’ without the $bootstrap->run($app); line, instead of the run-method I call “getObjectManager” and work with the object-manager for my tests.

    With these information I started my analysis how magento does it and how could I use the “EavSetup” for my purposes.
    Here you can see some classes that are relevant in instantiating an EavSetup:

    • \Magento\Setup\Model\ObjectManagerProvider
    • \Magento\Setup\Module\DataSetupFactory
    • \Zend\ServiceManager\ServiceManager
    • \Magento\Framework\ObjectManager\Factory\Compiled::create
    • \Magento\Framework\ObjectManager\Config\Compiled::getArguments

    The main issue was that \Magento\Framework\ObjectManager\Config\Compiled::getArguments  did not return the correct arguments.
    So while I was not able to pass the arguments as needed, I had to figure out another way how to use the “EavSetup” in my class.

    After debugging the magento instantiation of the “EavSetup”, I started to forge the magento process in my construct.

    The Solution

    The resulting solution looks something like this

    /**
     * SomeAttribute constructor.
     *
     * @param \Some\Own\AttributeContext $context
     * @param \Magento\Eav\Setup\EavSetupFactory $eavSetupFactory
     * @param string $entityTypeId
     *
     */
    public function __construct(
        \Some\Own\AttributeContext $context,
        \Magento\Eav\Setup\EavSetupFactory $eavSetupFactory,
        $entityTypeId = Product::ENTITY
    ) {
        parent::__construct($context);
    
        $serviceLocator = new \Zend\ServiceManager\ServiceManager();
        $serviceLocator->setService(\Magento\Setup\Mvc\Bootstrap\InitParamListener::BOOTSTRAP_PARAM, []);
    
        $provider = new \Magento\Setup\Model\ObjectManagerProvider($serviceLocator);
    
        $dataSetupFactory = new \Magento\Setup\Module\DataSetupFactory($provider);
        /** @var \Magento\Framework\Setup\SchemaSetupInterface | \Magento\Framework\Setup\ModuleDataSetupInterface $setup */
        $setup = $dataSetupFactory->create();
        $this->eavSetup = $eavSetupFactory->create(['setup' => $setup]);
        $this->entityTypeId = $entityTypeId;
    }

    Let me just explain in a few words what I had to do.

    For a better understanding let us move from bottom to top, my goal was to create a “EavSetup” instance to reach this goal I had to use the EavFactorySetup->create()  method.
    The EavSetupFactory has a dependency to the DataSetupFactory which itself needs a ObjectManagerProvider instance injected.
    This is the interesting point in this manual dependency injection flow:
    the ObjectManagerProvider now needs a ServiceManager to create the instance.  
    That’s the main cause of our issue with di:compile and production mode. This dependency cannot be injected by the default ObjectManager and thus the above mentioned error occured.

    So after manually building the dependencies and creating the objects, we could work around this issue.

    In case you haven’t seen how the “setup/index.php” “application” works you should have a closer look at that.

  • How to add alternative HTTP headers to Magento 2?

    If you have more than one frontend server running in your business, it’s needed to load balance the traffic between the nodes. In this case we have a new instance between the browser and the web-server.

    Often it’s a system like HAProxy or Varnish.

    Simple load balancer/proxy setup

    If the load balancer or proxy receives a request from a browser, it forwards it to backend server in the internal network. The IP address of the client is than added to a forward header which contains the IP address of a forward chain.

    Example:

    X-Forwarded-For: client, proxy1, proxy2

    In some situation i.e. for GEO-IP checks, you need the real IP address of a client. If you don’t configure Magento the remote address is always 127.0.0.1.

    That’s not what we want. We need the first part of the comma separated list of the IP chain. Magento offers us a mechanism to solve this issue.

    Remote Address

    The key to solve the issue is the class Magento\Framework\HTTP\PhpEnvironment\RemoteAddress which is provided by the Magento 2 framework.

    In Magento 2 it’s prohibited to call the $_SERVER['REMOTE_ADD'] directly. The RemoteAddress class is a wrapper to deal with the remote address.

    The correct way to get the remote address is this:
    use Magento\Framework\HTTP\PhpEnvironment\RemoteAddress;

    use Magento\Framework\HTTP\PhpEnvironment\RemoteAddress;
    
    class MyClass
    {
        /**
         * @var RemoteAddress
         */
        private $remoteAddress;
    
        public function __construct(RemoteAddress $remoteAddress)
        {
            $this->remoteAddress = $remoteAddress;
        }
    
        public function doSomething()
        {
            $ipAddressOfTheClient = $this->remoteAddress->getRemoteAddress();
        }
    }
    

    Configuration

    It’s possible to inject a list of alternative headers to the RemoteAddress class by Dependency Injection. This config isn’t related to any module. It’s a special config for a production server setup.

    It’s important to know that Magento 2 will load any „di.xml“ file from any subfolder of the „app/etc“ folder!

    With this information we can create a subfolder like „app/etc/myproject/di.xml“ with the following content:

    <?xml version="1.0"?>
    <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
            xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd">
    
        <type name="Magento\Framework\HTTP\PhpEnvironment\RemoteAddress">
            <arguments>
                <argument name="alternativeHeaders" xsi:type="array">
                    <item name="x-forwarded-for" xsi:type="string">HTTP_X_FORWARDED_FOR</item>
                </argument>
            </arguments>
        </type>
    </config>

     

    After that Magento will look into the given HTTP header „X-Forwarded-For“. The name of the header is normalized by PHP.
    X-Forwarded-For is available as $_SERVER['HTTP_XFORWARDED_FOR'].

    It’s possible to add more than one header to alternative header list.

    That’s it. Have fun with Magento 2. 🙂