В последнем посте я представил функцию в текущем проекте моей команды, которая позволит нашим пользователям (и нам!) Динамически определять «активы», и объяснил синтаксис для определения актива.
Теперь, когда у нас есть наше определение, нам нужно создать экземпляр актива. Здесь задействовано несколько классов: 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.