Как получить доступ к значениям вложенных параметров в Symfony2?

Я создал параметр в моем файле parameters.yml:

parameters:
    category:
        var: test

Как мне получить доступ к этому параметру в моем config.yml? Например, я хочу передать этот параметр во все мои файлы twig в качестве глобальной переменной ветки:

twig:
    globals:
        my_var: %category.var% # throws ParameterNotFoundException

Ответ 1

Во всех конфигурационных файлах symfony, которые я видел, записи в параметрах ":" всегда были полностью квалифицированы. Я не совсем понимаю, почему это так, но может помочь вам написать записи в ваших параметрах .yml вот так:

category1.var1: xxx
category1.var2: yyy
category1.var3. zzz

category2.subcategory1.var1: 5
category2.subcategory1.var2: 10
category2.subcategory2.var1: foo
category2.subcategory2.var2: bar

... и т.д.

ИЗМЕНИТЬ

Я попытался вставить вложенный параметр из вопроса в параметры .local.yml в одном из моих проектов и выполнил тривиальный unit test для извлечения этого и полностью квалифицированного параметра из контейнера, например.

$testUserEmail = $container->getParameter('test.user.email');
$this->assertEquals('[email protected]', $testUserEmail);
$testParam = $container->getParameter('category.var');
$this->assertEquals('test', $testParam);

Полностью соответствующий параметр был прав, пытаясь получить вложенный параметр в InvalidArgumentException: должен быть определен параметр category.var. Я не думаю, что параметры могут быть определены с помощью вложенности.

Ответ 2

Или вы можете написать простой ParameterBag, который может реализовать вложенные параметры динамически (без дублирования значений конфигурации):

<?php

namespace Your\Namespace;

use Symfony\Component\DependencyInjection\ParameterBag\ParameterBag;
use Symfony\Component\DependencyInjection\Exception\ParameterNotFoundException;

class ParameterBagNested extends ParameterBag
{

    /**
     * wire $this->set() logic into add() too
     *
     * @param array $parameters
     */
    public function add( array $parameters )
    {
        foreach ( $parameters as $name => $value ) {
            $this->set( $name, $value );
        }
    }

    /**
     * sets all levels of nested array parameters with dot notation
     * - loggly[host: loggly.com] will be translated this way:
     *  - loggly: [host: loggly.com] - standard array parameter will be left as is
     *  - loggly.host: loggly.com - nested variables ar translated so you can access them directly too as parent.variable
     *
     * @param string $name
     * @param mixed $value
     */
    public function set( $name, $value )
    {
        if ( $this->has( $name ) ) {
            // this is required because of array values
            // we can have arrays defined there, so we need to remove them first
            // otherwise some subvalues would to remain in the system and as a result, arrays would be merged, not overwriten by set()
            $this->remove( $name );
        }
        $this->setNested( $name, $value );
    }

    /**
     * remove checks even if name is not array
     *
     * @param string $name
     */
    public function remove( $name )
    {
        $value = $this->get( $name );
        if ( is_array( $value ) ) {
            foreach ( $value as $k => $v ) {
                $this->remove( $name . '.' . $k, $v );
            }
        }
        if ( strpos( $name, '.' ) !== FALSE ) {
            $parts = explode( '.', $name );
            $nameTopLevel = reset( $parts );
            array_shift( $parts );
            $topLevelData = $this->removeKeyByAddress( $this->get( $nameTopLevel ), $parts );
            ksort( $topLevelData );
            $this->setNested( $nameTopLevel, $topLevelData );
        }
        parent::remove( $name );
    }

    /**
     * @param array $data
     * @param array $addressParts
     *
     * @return array
     */
    private function removeKeyByAddress( $data, $addressParts )
    {
        $updatedLevel = & $data;
        $i = 1;
        foreach ( $addressParts as $part ) {
            if ( $i === count( $addressParts ) ) {
                unset( $updatedLevel[$part] );
            } else {
                $updatedLevel = & $updatedLevel[$part];
                $i++;
            }
        }
        return $data;
    }

    /**
     * @see set()
     *
     * @param string $name
     * @param mixed $value
     */
    private function setNested( $name, $value )
    {
        if ( is_array( $value ) ) {
            foreach ( $value as $k => $v ) {
                $this->setNested( $name . '.' . $k, $v );
            }
        }
        parent::set( $name, $value );
    }

}

Тест phpunit:

<?php

namespace Your\Namespace;

use Symfony\Component\DependencyInjection\Tests\ParameterBag\ParameterBagTest;

/**
 * its essential to use ParameterBagNested as ParameterBag because this way we run even parent class tests upon it
 * parent class is part of Symfony DIC standard test suite and we use it here just for check if our parameter bag is still ok
 */
use SBKS\DependencyInjection\ParameterBag\ParameterBagNested as ParameterBag;

/**
 * testing basic and even added ParameterBag functionality
 */
class ParameterBagNestedTest extends ParameterBagTest
{

    public function testConstructorNested()
    {
        $bag = new ParameterBag(
            array(
                'foo' => array( 'foo1' => 'foo' ),
                'bar' => 'bar',
            )
        );
        $this->assertEquals(
             array(
                 'foo.foo1' => 'foo',
                 'foo' => array(
                     'foo1' => 'foo',
                 ),
                 'bar' => 'bar',
             ),
             $bag->all(),
             '__construct() takes an array of parameters as its first argument'
        );
    }

    public function testRemoveNested()
    {
        $bag = new ParameterBag(
            array(
                'foo' => array(
                    'foo1' => array(
                        'foo11' => 'foo',
                        'foo12' => 'foo',
                    ),
                    'foo2' => 'foo',
                ),
                'bar' => 'bar',
            )
        );

        $bag->remove( 'foo.foo1.foo11' );
        $this->assertEquals(
             array(
                 'foo' => array(
                     'foo1' => array(
                         'foo12' => 'foo',
                     ),
                     'foo2' => 'foo',
                 ),
                 'foo.foo1' => array( 'foo12' => 'foo' ),
                 'foo.foo1.foo12' => 'foo',
                 'foo.foo2' => 'foo',
                 'bar' => 'bar',
             ),
             $bag->all(),
             '->remove() removes a parameter'
        );

        $bag->remove( 'foo' );
        $this->assertEquals(
             array(
                 'bar' => 'bar',
             ),
             $bag->all(),
             '->remove() removes a parameter'
        );
    }

    public function testSetNested()
    {
        $bag = new ParameterBag(
            array(
                'foo' => array(
                    'foo1' => array(
                        'foo11' => 'foo',
                        'foo12' => 'foo',
                    ),
                    'foo2' => 'foo',
                ),
            )
        );
        $bag->set( 'foo', 'foo' );
        $this->assertEquals( array( 'foo' => 'foo' ), $bag->all(), '->set() sets the value of a new parameter' );
    }

    public function testHasNested()
    {
        $bag = new ParameterBag(
            array(
                'foo' => array(
                    'foo1' => array(
                        'foo11' => 'foo',
                        'foo12' => 'foo',
                    ),
                    'foo2' => 'foo',
                ),
            )
        );
        $this->assertTrue( $bag->has( 'foo' ), '->has() returns true if a parameter is defined' );
        $this->assertTrue( $bag->has( 'foo.foo1' ), '->has() returns true if a parameter is defined' );
        $this->assertTrue( $bag->has( 'foo.foo1.foo12' ), '->has() returns true if a parameter is defined' );
        $this->assertTrue( $bag->has( 'foo.foo2' ), '->has() returns true if a parameter is defined' );
    }

}

Затем вы можете использовать его вставляя пакет Parameter в ContainerBuilder:

$parameterBag = new \Your\Namespace\ParameterBagNested();
$container = new ContainerBuilder($parameterBag);

Вот и все, теперь вы можете использовать вложенные параметры с точечной записью в контейнере Symfony DI.

Если вы используете плагин Symfony в phpstorm, он автоматически заполнит ваши вложенные атрибуты с помощью точечной нотации.

Ответ 3

$this->container->getParameter('category')['var']

Я тестировал это на symfony 2.8, и это сработало для меня.

Ответ 4

Немного поздно, но вот решение, которое сработало для меня.

# app/config/config.yml
twig:
    globals:
        my_var: category['var']

объяснение

Когда вы импортируете ваш файл параметров в ваш файл app/config/config.yml, вы можете автоматически получить доступ ко всем переменным, определенным в вашем файле параметров.

Имейте в виду, когда вы используете структуру:

# app/config/parameters.yml
parameters:
    category:
        var: test

Вы определяете параметр под названием category со значением, которое, в свою очередь, само по себе является массивом пары ключ-значение, который содержит var качестве ключа и test качестве значения.

Обновить

В более новых версиях Symfony (3.4 - 4.2) вы можете делать следующее:

# app/config/config.yml
twig:
    globals:
       custom_categories: '%categories%'

Установив этот параметр:

# app/config/parameters.yml
parameters:
    categories:       
      - category_A
      - category_B

На что нужно обратить внимание: - в settings.yml Categories это массив

поэтому, чтобы использовать его в шаблоне ветки, вы должны иметь возможность сделать что-то вроде:

<ul>
{% for category in categories %}
    <li> {{ category }} </li>
{% endfor %}
</ul>

Еще один пример с объектами:

# app/config/parameters.yml
parameters:
    marketplace:       
        name: 'My Store'
        address: '...'

Настройка переменных веток:

# app/config/config.yml
twig:
    globals:
       marketplace: '%marketplace%'

Используя это в ветке:

...
<p>{{marketplace.name}}</p>
<p>{{marketplace.address}}</p>
...

Надеюсь это поможет! :)

Ответ 5

Чтобы обеспечить альтернативный подход, который не требует удаления значений из сеттера, вместо этого я переопределяю метод getter.

namespace NameSpaceFor\ParameterBags;

use Symfony\Component\DependencyInjection\Exception\ParameterNotFoundException;
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBag;


class ParameterBagParser extends ParameterBag
{

    /**
     * {@inheritDoc}
     */
    public function get($name, $parent = null)
    {
        if (null === $parent) {
            $parent = $this->parameters;
        }
        $name = strtolower($name);
        if (!array_key_exists($name, $parent)) {
            if (!$name) {
                throw new ParameterNotFoundException($name);
            }
            if (false !== strpos($name, '.')) {
                $parts = explode('.', $name);
                $key = array_shift($parts);
                if (isset($parent[$key])) {
                    return $this->get(implode('.', $parts), $parent[$key]);
                }
            }
            $alternatives = [];
            foreach ($parent as $key => $parameterValue) {
                $lev = levenshtein($name, $key);
                if ($lev <= strlen($name) / 3 || false !== strpos($key, $name)) {
                    $alternatives[] = $key;
                }
            }
            throw new ParameterNotFoundException($name, null, null, null, $alternatives);
        }

        return $parent[$name];
    }

}

Он рекурсивно пересекает имя до тех пор, пока не будут отмечены все точечные обозначения.

Таким образом, он будет работать с массивами и скалярными значениями.

config.yml:

parameters:
   my_param:
     - test

   my_inherited: '%my_param.0%' #test

ContainerAware:

$container->getParameter('my_param')[0]; //test

Ответ 6

Хитрость для эмуляции вложенных параметров, доступ в файл yaml:

parameters:
  crawler:
    urls:
      a: '%crawler.urls.a%'   # here the root of the rest of the tree/array
  crawler.urls.a:
    cat1:
      - aa
      - bb
    cat2:
      - cc

services:
  xx:
    class:  myclass
    arguments:
      $urls:   '%crawler.urls.a%'

В Symfony я теперь обращаюсь к параметру ('crawler') как к полному дереву, в службе xx доступно поддерево/массив.