Factory Trait

trait FactoryTrait

Introduction

This trait is used to initialize object of the appropriate class, handling things like:

  • determining name of the class with ability to override
  • passing argument to constructors
  • setting default property values

Thanks to Factory trait, the following code:

$button = $app->add(['Button', 'A Label', 'icon'=>'book', 'action'=>My\Action::class]);

can replace this:

$button = new \atk4\ui\Button('A Label');
$button->icon = new \atk4\ui\Icon('book');
$button->action = new My\Action();
$app->add($button);

Type Hinting

Agile Toolkit 2.1 introduces support for a new syntax. It is functionally identical to a short-hand code, but your IDE will properly set type for a $button to be class Button instead of class View:

$button = Button::addTo($view, ['A Label', 'icon'=>'book', 'action'=>My\Action::class]);

The traditional $view->add will remain available, there are no plans to remove that syntax.

Class Name Resolution

Ability to specify only name of the class in ATK is used to keep code clean:

$app->initLayout('Centered');
$form = $app->add('Form');
$form->addField('food_selection', ['Lookup', 'model'=>Model\Food::class]);

In this snippet - ‘Centered’ refers to atk4uiLayoutCentered, ‘Form’ refers to atk4uiForm and ‘Lookup’ refers to atk4uiFormFieldLookup. Yet various methods are able to accept short identifiers for a simple syntax.

FactoryTrait::normalizeClassName($name, $prefix = null)

So App::initLayout() would call normalizeClassName($layout, ‘atk4uiLayout’), and View::add() would call normalizeClassName($view, ‘atk4ui’) and finally Form::addField() would call normalizeClassName($field, ‘atk4uiFormField to produce the actual name of the class.

ATK also fully supports the following two alternative forms:

use MyOwn\Layout;

$app->initLayout(Layout::class);

and:

$app->initLayout(new MyOwn\Layout());

In the first case Layout::class resolves into string “MyOwnLayout” by the PHP and normalizeClassName would leave it alone. In the former case, object is not even passed to normalizeClassName.

Danger of Global Namespace

PHP does not have a special type for a class. It simply uses string and dynamic nature of the language, but it may confuse normalizeClassName() when using classes defined in global namespace:

class TestLayout {
}

$app->initLayout(TestLayout::class);
// same as $app->initLayout('TestLayout');

In this case, the call to normalizeClassName would proceed to prefix ‘TestLayout’ layout into atk4uiLayoutTest.

The solution in this case is to use $app->initLayout(‘TestLayout’)

Name Resoluion Safety

While you specify name of the layout yourself, it is not a problem, but if you have a code like this:

$app->initLayout($_GET['layout']);

Although it seems harmless, technically argument can point to ANY class, which will be loaded and code in this class executed. This can be solved by using a ‘.’ prefix in front of the relative class name:

$app->initLayout('.'.$_GET['layout']);

// or

$app->initLayout('.Centered');

Sub-namespaces

Sometimes developers prefer to use sub-namespaces, so instead of atk4uiLayoutCentered_Login class IDE/Standards may convince developer to use atk4uiLayoutCentererdLogin. This becomes another problem for the resolution:

$app->initLayout('Centered\Login');

This actually looks like the a string generated by use Centered; Login::class and to avoid, the following syntax can be used:

$app->initLayout('Centered/Login');

Substituting / Overriding classes

One of the amazing features of ATK is ability to replace standard classes with your own. For example you may be using a 3rd party add-on which relies on the standard atk4uiForm class.

But what if you have extended this class and made some security enhancement to it? How to force 3rd party code to use it?

ATK provides a way:

  1. All add-ons must always use $v->add(‘Form’) format.
  2. You should define method normalizeClassName in your App class.

Example:

// class App
function normalizeClassName(string $name, string $prefix = null): string {
   if ($name == 'Form' && $prefix == 'atk4\ui') {
      return MyExtensions\SecureForm::class;
   }
}

There are other ways things you can do for example source components from various namespaces:

$layouts = ['my\addon1', 'my\addon2', 'atk4\ui'];

if ($prefix=='atk4\ui\Layout') {
   foreach($layouts as $layout) {
      $class = $layout.'\Layout\'.$name;
      if (class_exists($class) {
         return $class;
      }
   }
}

This can be quite handy for extending field types, column decorators and much much more.

Name Resolution Rules

When it comes to implementation, the rules for name resolution is as follows:

  1. If $app is defined for the object and $app->normalizeClassName exists, call it.
    1. if class name is returned use it.
    2. otherwise continue.
  2. If $name starts with ‘.’, remove it and apply prefix.
  3. or if $name contains ‘’, do not apply prefix
  4. otherwise apply prefix.

Finally if class name contains ‘/’ characters, replace them with ‘’.

Seed

Using “class” as opposed to initialized object yields many performance gains, as initialization of the class may be delayed until it’s required. For instance:

$model->hasMany('Invoices', Invoice::class);

// is faster than

$model->hasMany('Invoices', new Invoice());

That is due to the fact that creating instance of “Invoice” class is not required until you actually traverse into it using $model->ref(‘Invoices’) and can offer up to 20% performance increase. But in some cases, you want to pass some information into the object.

Suppose you want to add a button with an icon:

$button = $view->add('Button');
$button->icon = new Icon('book');

It’s possible that some call-back execution will come before button rendering, so it’s better to replace icon with the class:

$button = $view->add('Button');
$button->icon = Icon::class;

In this case, however - it is no longer possible to pass the “book” parameter to the constructor of the Icon class.

This problem is solved in ATK with “Seeds”.

A Seed is an array consisting of class name/object, named and numeric arguments:

$seed = [Button::class, 'My Label', 'icon'=>'book'];

Seed with and without class

There are two types of seeds - with class name and without. The one above contains the class and is used when user needs a flexibility to specify a class:

$app->add(['Button', 'My Label', 'icon'=>'book']);

The other seed type is class-less and can be used in situations where there are no ambiguity about which class is used:

$button->icon = ['book'];

Either of those seeds can be replaced with the Object:

$button = $app->add(new Button('My Label'));
$button->icon = new Icon('book');

If seed is a string then it would be treated as class name. For a class-less seed it would be treaded as a first argument to the construcor:

$button = $app->add('Button');
$button->icon = 'book';

Lifecycle of argument-bound seed

ATK only uses setters/getters when they make sense. Argument like “icon” is a very good example where getter is needed. Here is a typical lifecycle of an argument:

  1. when object is created “icon” is set to null
  2. seed may have a value for “icon” and can set it to string, array or object
  3. user may explicitly set “icon” to string, array or object
  4. some code may wish to interract with icon and will expect it to be object
  5. recursiveRender() will expect icon to be also added inside $button’s template

So here are some rules for ATK and add-ons:

  • use class-less seeds where possible, but indicate so in the comments
  • keep seed in its original form as long as possible
  • use getter (getIcon()) which would convert seed into object (if needed)
  • add icon object into render-tree inside recursiveRender() method

If you need some validation (e.g. icon and iconRight cannot be set at the same time by the button), do that inside recursiveRender() method or in a custom setter.

If you do resort to custom setters, make sure they return $this for better chaining.

Always try to keep things simple for others and also for yourself.

Factory

As mentioned juts above - at some point your “Seed” must be turned into Object. This is done by executing factory method.

FactoryTrait::factory($seed, $defaults = [])

Creates and returns new object. If is_object($seed), then it will be returned and $defaults will only be sed if object implement DIContainerTrait.

In a conventional PHP, you can create and configure object before passing it onto another object. This action is called “dependency injecting”. Consider this example:

$button = new Button('A Label');
$button->icon = new Icon('book');
$button->action = new Action(..);

Because Components can have many optional components, then setting them one-by-one is often inconvenient. Also may require to do it recursively, e.g. Action may have to be configured individually.

On top of that, there are also namespaces to consider and quite often you would want to use 3rdpartybootstrapButton() instead of default button.

Agile Core implements a mechanism to make that possible through using factory() method and specifying a seed argument:

$button = $this->factory(['Button', 'A Label', 'icon'=>['book'], 'action'=>new Action(..)]);

it has the same effect, but is shorter. Note that passing ‘icon’=>[‘book’] will also use factory to initialize icon object.

You can also use a new Button::class notation instead:

use atk4\ui\Button;

$button = $this->factory([Button::Class, 'A Label', 'icon'=>['book'], 'action'=>new Action(..)]);

Finally, if you are using IDE and type hinting, a preferred code would be:

use atk4\ui\Button;
$this->factory($button = new Button('A Label'), ['icon'=>['book'], 'action'=>new Action(..)]);

This will properly set type to $button variable, while still setting properties for icon/action. More commonly, however, you would use this through the add() method:

use atk4\ui\Button;

$view->add([$button = new Button('A Label'), 'icon'=>['book'], 'action'=>new Action('..')]);

Seed Components

Class definition - passed as the $seed[0] and is the only mandatory component, e.g:

$button = $this->factory(['Button']);

Any other numeric arguments will be passed as constructor arguments:

$button = $this->factory(['Button', 'My Label', 'red', 'big']);

// results in

new Button('My Label', 'red', 'big');

Finally any named values inside seed array will be assigned to class properties by using DIContainerTrait::setDefaults.

Factory uses array_shift to separate class definition from other components.

Class-less seeds

You cannot create object from a class-less seed, simply because factory would not know which class to use. However it can be passed as a second argument to the factory:

$this->icon = $this->factory(['Icon', 'book'], $this->icon);

This will use class icon and first argument ‘book’ as default, but would use exitsing seed version if it was specified. Also it will preserve the object value of an icon.

Factory Defaults

Defaults array takes place of $seed if $seed is missing components. $defaults is using identical format to seed, but without the class. If defaults is not an array, then it’s wrapped into [].

Array that lacks class is called defaults, e.g.:

$defaults = ['Label', 'My Label', 'big red', 'icon'=>'book'];

You can pass defaults as second argument to FactoryTrait::factory():

$button = $this->factory(['Button'], $defaults);

Executing code above will result in ‘Button’ class being used with ‘My Label’ as a caption and ‘big red’ class and ‘book’ icon.

You may also use null to skip an argument, for instance in the above example if you wish to change the label, but keep the class, use this:

$label = $this->factory([null, 'Other Label'], $defaults);

Finally, if you pass key/value pair inside seed with a value of null then default value will still be used:

$label = $this->factory(['icon'=>null], $defaults);

This will result icon=book. If you wish to disable icon, you should use false value:

$label = $this->factory(['icon'=>false], $defaults);

With this it’s handy to pass icon as an argument and don’t worry if the null is used.

Precedence and Usage

When both seed and defaults are used, then values inside “seed” will have precedence:

  • for named arguments any value specified in “seed” will fully override identical value from “defaults”, unless if the seed’s value is “null”.
  • for constructor arguments, the non-null values specified in “seed” will replace corresponding value from $defaults.

The next example will help you understand the precedence of different argument values. See my description below the example:

class RedButton extends Button {
    protected $icon = 'book';

    function init(): void {
        parent::init();

        $this->icon = 'right arrow';
    }
}

$button = $this->factory(['RedButton', 'icon'=>'cake'], ['icon'=>'thumbs up']);
// Question: what would be $button->icon value here?

Factory will start by merging the parameters and will discover that icon is specified in the seed and is also mentioned in the second argument - $defaults. The seed takes precedence, so icon=’cake’.

Factory will then create instance of RedButton with a default icon ‘book’. It will then execute DIContainerTrait::setDefaults with the [‘icon’=>’cake’] which will change value of $icon to cake.

The cake will be the final value of the example above. Even though init() method is set to change the value of icon, the init() method is only executed when object becomes part of RenderTree, but that’s not happening here.

Seed Merging

FactoryTrait::mergeSeeds($seed, $seed2, ...)

Two (or more) seeds can be merged resulting in a new seed with some combined properties:

  1. Class of a first seed will be selected. If specified as “null” will be picked
    from next seed.
  2. If string as passed as any of the argument it’s considered to be a class
  3. If object is passed as any of the argument, it will be used instead ignoring all classes and numeric arguments. All the key->value pairs will be merged and passed into setDefaults().

Some examples:

mergeSeeds(['Button', 'Button Label'], ['Message', 'Message label']);
// results in ['Button', 'Button Label']

mergeSeeds([null, 'Button Label'], ['Message', 'Message Label']);
// Results in ['Message', 'Button Label']);

mergeSeeds(['null, 'Label1', 'icon'=>'book'], ['icon'=>'coin', 'Button'], ['class'=>['red']]);
// Results in ['Button', 'Label1', 'icon'=>'book', 'class'=>['red']]

Seed merging can also be used to merge defaults:

mergeSeeds(['label 1'], ['icon'=>'book']);
// results in ['label 1', 'icon'=>'book']

When object is passed, it will take precedence and absorb all named arguments:

mergeSeeds(
    ['null, 'Label1', 'icon'=>'book'],
    ['icon'=>'coin', 'Button'],
    new Message('foobar'),
    ['class'=>['red']]
);
// result is
// $obj = new Message('foobar');
// $obj->setDefaults(['icon'=>'book', 'class'=>['red']);

If multiple objects are specified then early ones take precedence while still absorbing all named arguments.

Default and Seed objects

When object is passed as 2nd argument to factory() it takes precedence over all array-based seeds. If 1st argument of factory() is also object, then 1st argument object is used:

factory(['Icon', 'book'], ['pencil']);
// book

factory(['Icon', 'book'], new Icon('pencil')];
// pencil

factory(new Icon('book'), new Icon('pencil')];
// book

Usage in frameworks

There are several ways to use Seed Merging and Agile UI / Agile Data makes use of those patterns when possible.

Specify Icon for a Button

As you may know, Button class has icon property, which may be specified as a string, seed or object:

$button = $app->add(['Button', 'icon'=>'book']);

Well, to implement the button internally, render method uses this:

// in Form
$this->buttonSave = $this->factory(['Button'], $this->buttonSave);

So the value you specify for the icon will be passed as:

  • string: argument to constructor of Button().
  • array: arguments for constructors and inject properties
  • object: will override return value

Specify Layout

The first thing beginners learn about Agile Toolkit is how to specify layout:

$app = new \atk4\ui\App('Hello World');
$app->initLayout('Centered');

The argument for initLayout is passed to factory:

$this->layout = $this->factory($layout, null, 'Layout');

The value you specify will be treated like this:

  • string: specify a class (prefixed by Layout)
  • array: specify a class and allow to pass additional argument or constructor options
  • object: will override layout

Form::addField and Table::addColumn

Agile UI is using form field classes from namespace atk4uiFormField. A default class is ‘Line’ but there are several ways how it can be overridden:

  • User can specify $ui[‘form’] / $ui[‘table’] property for model’s field
  • User can pass 2nd parameter to addField()
  • Class can be inferred from field type

Each of the above can specify class name, so with 3 seed sources they need merging:

$seed = mergeSeeds($decorator, $field->ui, $inferred, ['Line', 'form' => $this]);
$decorator = factory($seed, null, 'FormField');

Passing an actual object anywhere will use it instead even if you specify seed.

Specify Form Field

addField, addButton, etc

Model::addField, Form::addButton, FormLayout::addHeader imply that the class of an added object is known so the argument you specify to those methods ends up being a factory’s $default:

function addButton($label) {
    return $this->add(
        $this->factory(['Button', null, 'secondary'], $label);
        'Buttons'
    );
}

in this code factory will use a seed with a null for label, which means, that label will be actually taken from a second argument. This pattern enables 3 ways to use addButton():

$form->addButton('click me');
// Adds a regular button with specified label, as expected

$form->addButton(['click me', 'red', 'icon'=>'book']);
// Specify class of a button and also icon

$form->addButton(new MyButton('click me'));
// Use an object specified instead of a button

A same logic can be applied to addField:

$model->addField('is_vip', ['type'=>'boolean']);
// class = Field, type = boolean

$model->addField('is_vip', ['boolean'])
// new Field('boolean'), same result

$model->addField('is_vip', new MyBoolean());
// new MyBoolean()

and the implementation uses factory’s default:

$field = $this->factory($this->_field_class, $arg, 'atk4\data');

Normally the field class property is a string, which will be used, but it can also be array.

OBSOLETE - Namespace

You might have noticed, that seeds do not specify namespace. This is because factory relies on $app to normalize your class name.

FactoryTrait::normalizeClassName($name, $prefix = null)

Motivation

I have created namespace prefixing as described here for the following reasons:

  • PHP has capability to create class names out of strings, unlike compiled languages that have type safety. I see that as a benefit and a feature of PHP so allowing namespace logic can lift some extra thinking from you.
  • Agile Toolkit is designed to be simple and powerful. The code which uses seeds is very easy to read and understand even for non-programmers.
  • Use of seeds have some great potential for extending. If someone is looking to integrate Agile UI with Drupal, they might need a specific functionality out of their ‘Button’ implementation. Use of seed allow integrator to substitute classes and redirect button class to a different namespace. Alternatively you would have to change “use” keywords making your code less portable.

Features

Class normalization and prefixing offer several good features to the rest of the framework:

  • Allow to work with App and without App.
  • Contextual prefixing is great for creating separate class namespaces: ‘Checkbox’ -> ‘FormFieldCheckbox’
  • Namespace prefix “FormLayout” can be used for discovery of possible classes.
  • Global prefixing logic can be quite sophisticated and implemented inside App.
  • Use of forward slashes helps avoid errors

Seed class

Here are some Seed usage examples. First the basic usage where a class specified by you “Button” is converted into \atk4\ui\Button:

// \atk4\ui\Button
$app->add(['Button', 'My Label']);

// \atk4\ui\Button\WithDropdown - (non-existant class)
$app->add(['Button\WithDropdown', 'My Label']);

// \MyNamespace\Button
$app->add([\MyNamespace\Button::class, 'My Label']);

Contextual Prefix

Methods such as $form->addField() or $app->initLayout() often use prefixing:

function initLayout($layout) {
    $this->layout = $this->factory($layout, ['app'=>$this], 'Layout');
}

The above method can then be used with string argument, array or even object and will still work consistently. If you specify ‘Centered’ layout, then it will be prefixed with ‘LayoutCentered’.

This is called Contextual Prefix and is used in various methods throughout Agile Toolkit:

  • Form::addField(‘age’, [‘Hidden’]); // uses FormFieldHidden class
  • Table::addColumn(‘status’, [‘Checkbox’]); // uses TableColumnCheckbox class
  • App::initLayout(‘Admin’); // uses LayoutAdmin class

Here are some examples of contextual prefixing:

// \atk4\ui\FormField\Checkbox
$form = $app->add('Form');
$form->addField('agree_to_terms', 'Checkbox', 'I Agree to Terms of Service');

// \MyNamespace\Checkbox
$form = $app->add('Form');
$form->addField('agree_to_terms', \MyNamespace\Checkbox::class, 'I Agree to Terms of Service');

Specifying contextual prefix will still leave it up for global prefixing, but if you want to fully specify a namespace, then you can use \Prefix. If you build your own component in your own namespace with some features, you can use this technique:

namespace my\auth;

class AuthController {
    use FactoryTrait;    // implements $this->factory
    use AppScopeTrait;   // links $this->app
    use ContainerTrait;  // implements $this->add

    function enableFeature($feature) {
        return $this->add($this->factory($feature, ['myattr' => $this], 'my\auth\feature');
    }
}

To use this AuthController you would write:

$auth = $app->add('my\auth\AuthController');

// \my\auth\feature\facebook
$auth->enableFeature('facebook');

This contextual prefixing avoids global prefixing.

Global Prefix

Application class may specify how to add a global namespace. For example, atk4uiApp will use prefix class name with “atk4ui”, unless, of course, you override that somehow.

This is done, so that add-ons may intercept generation of Factory class and have control over the code like this:

$button = $this->add(['Button']);

By substituting atk4uiButton with a different button implementation. It’s even possible to verify if class exists before prefixing or use routing maps. Neither Agile Core nor Agile UI implement such logic but you can build your own $this->app->normalizeClassNameApp().

The next example will replace all the Grid classes with the one you have implemented inside your own namespace:

class MyApp extends \atk4\ui\App {
    function normalizeClassNameApp(string $name, string $prefix = null): ?string {
        if ($name == 'Grid') {
            return 'myextensions\Grid';
        }

        return parent::normalizeClassNameApp($name);
    }
}

Use with add()

ContainerTrait::add() will allow first argument to be Seed, but only if the object also uses FactoryTrait. This is exactly the case for Agile UI / View objects, so you can supply seed to add:

$grid = $app->add(['Grid', 'toolbar'=>false']);

Method add() however only takes one argument and you cannot specify defaults or prefix.

In most scenarios, you don’t have to use factory() directly, simply use add()