moderntribe / square-one

Modern Tribe's legacy WordPress framework for the classic editor.
GNU General Public License v2.0
89 stars 20 forks source link

Feature: acf field models / data transfer objects #983

Closed defunctl closed 2 years ago

defunctl commented 2 years ago

TLDR;

Automatically transform arrays into objects (Field Models/Data Transfer Objects) and gain automatic type casting, type safe models and portable objects. You never have to remember an array index name again. Directly pass in matching ACF fields and that's it:

// [ 'url' => 'https://tri.be', 'title' => 'Tribe', 'target' => '_self' ]
$link = new \Tribe\Libs\Field_Models\Models\Link( get_field( 'my_link_field' ) );

// These all auto complete for you in your IDE so you no longer have to remember 
// what properties are available for an ACF field that returns an array.
echo $link->url;
echo $link->title;
echo $link->target;

We've been using these in a number of projects already but I've had to manually bring them and convert everything, so this should be the last time :smile:.

T3 has a few new projects starting soon and this will save us a lot of time once it's built into the framework from the start.

What does this do/fix?

Why Field Models / DTO's?

  1. Field models/DTO's remove the inconsistent typing from ACF and allows you populate a Field Model with a single get_field() call. You'll always get the expected type back from a Field Model, e.g. $link->url will always be a string.
  2. IDE code completion. Set a Field Model once and pass around the instance you'll see all available properties without having to remember what an ACF field consists of. Do you know all the ACF File Field indexes and their types?
  3. Simpler and much less code (see before and after below). Fewer model/controller constants and because the Field Model travels through these instances, you only have to add a new property to the Field Model and then you can use it in the controller or view without adding additional controller and model constants/properties.
  4. Collections let you create many of the same Field Model, with full IDE completion, assuming you type hint the collection you made. example. Collections are extremely memory efficient, as they are iterators out of the box, so they can hold an extreme amount of data, and will use minimal memory when foreach() looping over them. They can be converted to an array, which will utilize full memory when absolutely necessary. Read more about collections.
  5. It's easy to make your own Field Models and Collections. Just extend the Field_Model class, add typed properties that match your ACF field names and your data will get auto-populated based on what get_field() returns.

These are the ACF models I've made so far out of the box for ACF, but any ACF field that returns an array can be made into one, so if you have any ideas, please submit a PR. If you want to know how things work under the hood, I think the integration tests do a good job of that.

You can bring the ACF models and DTO library into an older project if you wish, as long as it's PHP7.4+ with so composer require moderntribe/square1-field-models.


Code Examples

Some before and afters showing reduced code required.

Before: CTA

// compacted block model example
class Stats_Model extends Base_Model {

    public function get_data(): array {
        return [
            Stats_Block_Controller::ATTRS => $this->get_attrs(),
            Stats_Block_Controller::CTA   => $this->get_cta_args(),
        ];
    }

    private function get_cta_args(): array {
        $cta  = $this->get( Cta_Field::GROUP_CTA, [] );
        $link = wp_parse_args( $cta['link'] ?? [], [
            'title'  => '',
            'url'    => '',
            'target' => '',
        ] );

        return [
            Link_Controller::CONTENT        => $link['title'],
            Link_Controller::URL            => $link['url'],
            Link_Controller::TARGET         => $link['target'],
            Link_Controller::ADD_ARIA_LABEL => $cta['add_aria_label'] ?? false,
            Link_Controller::ARIA_LABEL     => $cta['aria_label'] ?? '',
        ];
    }
}

// compacted block controller
class Stats_Block_Controller extends Abstract_Controller {

    public const CTA = 'cta';

    /**
     * @var string[]
     */
    private array $cta;

    public function __construct( array $args = [] ) {
        $args      = $this->parse_args( $args );
        $this->cta = (array) $args[ self::CTA ];
    }

    public function get_header_args(): array {
        return [
            Content_Block_Controller::TAG => 'header',
            Content_Block_Controller::CTA => $this->get_cta(),
        ];
    }

    protected function defaults(): array {
        return [
            self::CTA => [],
        ];
    }

    private function get_cta(): Deferred_Component {
        $cta = wp_parse_args( $this->cta, [
            'content'        => '',
            'url'            => '',
            'target'         => '',
            'add_aria_label' => false,
            'aria_label'     => '',
        ] );

        return defer_template_part( 'components/link/link', null, [
            Link_Controller::URL            => $cta['url'],
            Link_Controller::CONTENT        => $cta['content'] ?: $cta['url'],
            Link_Controller::TARGET         => $cta['target'],
            Link_Controller::ADD_ARIA_LABEL => $cta['add_aria_label'],
            Link_Controller::ARIA_LABEL     => $cta['aria_label'],
            Link_Controller::CLASSES        => [
                'c-block__cta-link',
                'a-btn',
                'a-btn--has-icon-after',
                'icon-arrow-right',
            ],
        ] );
    }

}

After: CTA

// compacted block model example
class Stats_Model extends Base_Model {

    public function get_data(): array {
        return [
            Stats_Block_Controller::ATTRS => $this->get_attrs(),
            Stats_Block_Controller::CTA   => new Cta( $this->get( Cta_Field::GROUP_CTA, [] ) ),
        ];
    }

}

// compacted block controller
class Stats_Block_Controller extends Abstract_Controller {

    public const CTA = 'cta';

    private Cta $cta;

    public function __construct( array $args = [] ) {
        $args      = $this->parse_args( $args );
        $this->cta = $args[ self::CTA ];
    }

    public function get_header_args(): array {
        return [
            Content_Block_Controller::TAG => 'header',
            Content_Block_Controller::CTA => $this->get_cta(),
        ];
    }

    protected function defaults(): array {
        return [
            self::CTA => new Cta(),
        ];
    }

    private function get_cta(): Deferred_Component {
        return defer_template_part( 'components/link/link', null, [
            Link_Controller::URL            => $this->cta->link->url,
            Link_Controller::CONTENT        => $this->cta->link->title ?: $this->cta->link->url,
            Link_Controller::TARGET         => $this->cta->link->target,
            Link_Controller::ADD_ARIA_LABEL => $this->cta->add_aria_label,
            Link_Controller::ARIA_LABEL     => $this->cta->aria_label,
            Link_Controller::CLASSES        => [
                'c-block__cta-link',
                'a-btn',
                'a-btn--has-icon-after',
                'icon-arrow-right',
            ],
        ] );
    }

}

Take an ACF Link Field as an example. If not populated, get_field( 'my_link_field' ); can return null, false, an empty array etc...

Take our Link Field Model and just pass in the field output:

// get_field() outputs this data: [ 'url' => 'https://tri.be', 'title' => 'Tribe', 'target' => '_self' ]
$link = new \Tribe\Libs\Field_Models\Models\Link( get_field( 'my_link_field' ) );

// Need an array, try...
$link->toArray(); // [ 'url' => 'https://tri.be', 'title' => 'Tribe', 'target' => '_self' ]

// Need only some of the field data?
$link->only( 'url' )->toArray();  // [ 'url' => 'https://tri.be' ]
$link->except( 'target' )->toArray();  // [ 'url' => 'https://tri.be', 'title' => 'Tribe' ]

QA

Tests

Does this have tests?