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