Статьи

Построение динамических активов — PHP Project pt. я

В последнем посте я представил функцию в текущем проекте моей команды, которая позволит нашим пользователям (и нам!) Динамически определять «активы», и объяснил синтаксис для определения актива.

Теперь, когда у нас есть наше определение, нам нужно создать экземпляр актива. Здесь задействовано несколько классов: AssetField, который хранит отображаемую и проверочную информацию, содержащуюся в определении для одного поля; Актив, который в основном является контейнером, обернутым вокруг списка полей и значений полей; и AssetFactory, которая читает определение и создает Актив, вешая на него поля.

В приведенном ниже коде я намеренно опускаю большинство мрачных деталей, потому что они скучные, и я больше концентрируюсь на дизайне, чем на реализации.

AssetField — это, по сути, перевод кода определения JSON. Вот как это выглядит:

class AssetField
{
    const TypeString   = 'string';
    const TypeInteger  = 'int';
    // etc. for each type
    
    protected $name = null;
    protected $display = null;
    protected $type = self::TypeString;
    // etc. for each attribute that can exist in a field definition
    
    public function getName()
    {
        return $this->name;
    }
    
    // All setters return $this to provide a fluent interface
    public function setName($name)
    {
        $this->name = $name;
        return $this;
    }

    // ... other getters and setters for each property
    // listed in the definition attributes ...
}

// Create a field named "power_sources" whose value is a unique array
// of one or more of the values "Gas", "Electric", "Solar"
// which defaults to having Gas and Solar turned on
// and allows the user to enter their own value if they need to.
$powerSourceField = new AssetField();
$powerSourceField->setName("power_sources")
    ->setUnique(true)
    ->setCollection(true)
    ->setOptions(array("Gas", "Electric", "Solar"))
    ->setDefault(array("Gas", "Solar"));
    ->setOther(true);

Каждый объект Asset будет нести свои объекты AssetField, чтобы он мог предоставить информацию о том, как он построен. Это позволяет нашему построению формы и коду проверки быть очень общим.

Так как Assets не знает, какие поля будут на них висеть, мы используем магические методы PHP `__get` и` __set` для установки и возврата значений полей. Однако во время реализации мы поняли, что времена, когда мы хотим узнать значение поля, очень редки; чаще мы хотим знать свойства поля. Поэтому мы также используем магию `__call`, чтобы предоставить нашему коду доступ к базовому объекту поля.

Вот как выглядит большая часть класса Asset:

class Asset
{
    protected $type = null;
    protected $display = null;

    protected $fields = array();
    protected $values = array();

    // Return the field object when its name is called as a class method
    public function __call($fieldName, $args)
    {
        return isset($this->fields[$fieldName]) ? $this->fields[$fieldName] : null;
    }

    // Get the value of the named field
    public function __get($fieldName)
    {
        return isset($this->fields[$fieldName]) ? $this->values[$fieldName] : null;
    }

    // Set the value of the named field
    public function __set($fieldName, $value)
    {
        if (isset($this->fields[$fieldName])) {
           $this->values[$fieldName] = value;
        }
    }

    // Hang a new field on this asset
    public function addField(AssetField $field)
    {
        $name = $field->getName();
        $this->fields[$name] = $field;
        $this->values[$name] = $field->getDefault();
        return $this;
    }

    // ... Helper methods for returning the list of all fields
    // setting the Asset type, display string and instance name format ...
}

// Get an instantiation of our plumbing system example
$plumbing = new Asset();
$plumbing->setType("plumbing")
    ->setDisplay("Home Plumbing")
    ->setInstanceNameFormat("Installed %installation_date%");

$waterSource = new AssetField();
$waterSource->setName("water_source")
    ->setOptions(array("city", "well"))
    ->setOther("Where does the water come from")
    ->setDefault("city");

$installationDate = new AssetField();
$installationDate->setName("installation_date")
    ->setType(AssetField::TypeDate)
    ->setRequired(true);

$waterHeater = new AssetField();
$waterHeater->setName("water_heater")
    ->setType(AssetField::TypeSub)
    ->setOptions(array("gas_heater", "electric_heater"));

$showers = new AssetField();
$showers->setName("showers")
    ->setType(AssetField::TypeSub)
    ->setOptions(array("shower"))
    ->setCollection(true)
    ->setMax(5)
    ->setDefault(array());

$plumbing->addField($waterSource)
    ->addField($installationDate)
    ->addField($waterHeater)
    ->addField($showers);

// Use the asset 
$plumbing->water_source = "well";
$plumbing->installation_date = "06/05/2004";

echo $plumbing->getDisplay() . ": $plumbing";
// "Home Plumbing: Installed 06/05/2004"  <--- comes from an overloaded __toString method

echo "My water comes from a {$plumbing->water_source}"
// "My water comes from a well"

// Instantiate a new shower asset
$shower = new Asset();
// ... set up the asset ...

// add the shower to the plumbing system
$plumbing->showers[] = $shower;

// What is the default value for the water source?
$field = $plumbing->water_source();
$default = $field->getDefault();

Вызов поля как метода вернет объект AssetField для этого свойства. Затем объект поля можно использовать в формах и валидации. Еще одним преимуществом динамического построения наших активов таким образом является то, что мы можем настроить любой актив на лету, не затрагивая другие активы этого типа. Из нашего примера с сантехникой, давайте предположим, что один пользователь хочет отслеживать серийный номер своего водонагревателя, но никто другой этого не делает. Мы просто следим за тем, чтобы любой экземпляр ресурса water_heater для этого пользователя получал дополнительное поле «serial_number»:

// Continuing from above
if ($userId == $customAssetUserId) {
    $serialNumber = new AssetField();
    $serialNumber->setName("serial_number");
    $plumbing->addField($serialNumber);
}

Любой код создания и проверки формы автоматически выберет это поле и отобразит его для этого пользователя.

Последним важным классом для создания активов является AssetFactory. Все экземпляры активов создаются с помощью этой фабрики. Требуется определение типа, которое в примере с сантехникой представляет собой строку JSON. Фабрике на самом деле все равно, откуда берутся определения и как они хранятся, если они получают правильно отформатированный массив. AssetFactory получает определения, а затем использует определения для создания активов по требованию:

class AssetFactory
{
    // List of asset recipes this factory knows how to bake
    protected $definitions = array();

    public function define($definition)
    {
        // ... Validate proper formed-ness of the definition ...
        // ... Set some reasonable defaults for non-specified field attributes ...

        // All assets get an id field
        $definition['fields']['id'] = array(
            'type' => DW_Asset_Field::TypeString,
            'hidden' => true,
        );

        $this->definitions[$definition['type']] = $definition;
    }

    // Construct an asset of the given type
    public function build($type)
    {
        $definition = $this->definitions[$type];

        $asset = new Asset();
        $asset->setType($definition['type'])
            ->setDisplay($definition['display'])
            ->setInstanceNameFormat($format);

        foreach ($definition['fields'] as $name => $fieldDef) {
            $field = new AssetField();
            $field->setName($name)
                ->setType($fieldDef['type'])
                ->setDisplay($fieldDef['display'])
                ->setHidden($fieldDef['hidden'])
                ->setUnique($fieldDef['unique'])
                ->setRequired($fieldDef['required'])
                ->setMin($fieldDef['min'])
                ->setMax($fieldDef['max'])
                ->setOptions($fieldDef['options'])
                ->setOther($fieldDef['other'])
                ->setDefault($fieldDef['default']);

            $asset->addField($field);
            $asset->$name = $fieldDef['default'];
        }

        return $asset;
    }
}

$factory = new AssetFactory();
$factory->define(json_decode('{
    "type" : "plumbing",
    "display" : "Home Plumbing",
    "instance_name" : "Installed %installation_date%",
    "fields" : {
      "water_source" : {
        "type" : "string",
        "options" : ["city", "well"],
        "other" : "Where does the water come from",
        "default" : "city"
      },
      "installation_date" : {
        "type" : "date",
        "required" : true
      },
      "water_heater" : {
        "type" : "subasset",
        "options" : ["gas_heater", "electric_heater"],
      },
      "showers" : {
        "type" : "subasset",
        "options" : ["shower"],
        "collection" : true,
        "max" : 5
      }
    }
  }'));

$plumbing = $factory->build("plumbing");

Если у нас есть какие-либо настройки, как, например, наш serial_number сверху, мы можем вызвать метод `customize ()` на фабрике из метода `build ()` или передать результат `build ()` какой-либо другой настройке класс. На данный момент у нас нет таких требований. Важно то, что теперь у нас есть объект Asset, который мы можем передать нашему общему коду обработки активов.

Далее, описание того, как мы генерируем форму из универсального объекта Asset.