Cannot install a Sage theme via Composer

A Sage derived theme is not installable via Composer (for example, as a dependency within a Bedrock project), this seems like a huge oversights to me?

I’m aware that this has been discussed already (e.g. here), but the solutions there are not practical. I see that the issue originates from the soberwp/controller dependency, but it doesn’t look likely that’ll be fixed any time soon.

Fatal error: Uncaught Symfony\Component\Debug\Exception\FatalThrowableError: Class 'App' not found in /home/vagrant/code/mysite/public/app/uploads/cache/4d46a5e45eb9d60a7228a33nebcecef896e9d376.php on line 2

Something as simple as this makes Sage a no-go for me which is a real shame.

Has anybody managed to get around this issue at all? I saw somewhere that there is a new roots controller/view controller in the works (Acorn), although very early stages. Can we expect the new controller to work in a more standardised manner (i.e. doesn’t cause things to break under normal composer usage)?

I largely switched to just using Sage’s built-in filter-based functionality: https://roots.io/sage/docs/blade-templates/#passing-data-to-templates It provides the same stuff as soberwp/controller but doesn’t have the drawbacks you’ve described.

Thanks, that might be the way to go.

I guess the main drawback is that there’s no class structure. How do you handle structuring your template hooks, do you just stick them all in app/filters.php?

Which filter is used to replicate the global variables provided by the default App Controller (e.g. App::title())

Does something like: sage/template/*/data, sage/template/app/data exist?

(Sorry for all the questions, the documentation is kind of lacking)

You can structure them however you want. On the last project where I used them, I had a file structure that looked like this:

/theme
  /app
    /controllers
      /post-type
        single.php
        archive.php
      page.php
      front-page.php
    loader.php
  /resources
    functions.php
// theme/app/controllers/loader.php
array_map(function ($file) {
    $file = "../app/controllers/{$file}.php";
    if (!locate_template($file, true, true)) {
        printf(__('Error locating <code>%s</code> for inclusion.', 'sage'), $file);
    }
}, [
    'front-page',
    'page',
    'post-type/single.php',
    'post-type/archive.php',
]);

// theme/resources/functions.php
array_map(function ($file) use ($sage_error) {
    $file = "../app/{$file}.php";
    if (!locate_template($file, true, true)) {
        $sage_error(sprintf(__('Error locating <code>%s</code> for inclusion.', 'sage'), $file), 'File not found');
    }
}, [
    ...
    'controllers/loader',
]);

There is no built-in app-level target, but it’s easy to add one:

add_filter('body_class', function (array $classes) {
    /** 
     * I add this in filters.php, before any other classes, so that later
     * filters can override what it does.
     */
    array_unshift($classes, 'app');
});
// app/controllers/app.php
add_filter('sage/template/app/data', function ($data) {
    if (is_home()) {
        $title = __('Latest Posts', 'sage');
        if ($home = get_option('page_for_posts', true)) {
            $title = get_the_title($home);
        }
    } elseif (is_archive()) {
        $title = get_the_archive_title();
    } elseif (is_search()) {
        $title = sprintf(__('Searched Term: %s', 'sage'), get_search_query());
    } elseif (is_404()) {
        $title = __('Not Found', 'sage');
    } else {
        $title = get_the_title();
    }
    $data['title'] = $title;

    return $data;
});

// resources/views/some-blade.blade.php
<h1>{{ $title }}</h1>
2 Likes

Thanks for that. I’ve rolled my own class-based solution mainly to make use of psr-4 autoloading in app/, so no need to register the filters/files manually. For anybody else interested in removing soberwp/controller, but still after a more OOP approach…

Update Sage’s template_include filter in filters.php to:

/**
 * Render page using Blade
 */
add_filter('template_include', function ($template) {
    $data = collect(get_body_class())->reduce(function ($data, $class) use ($template) {
        
        // Initialise the data injector if it exists:
        $injector_class = __NAMESPACE__ . '\Injectors\\' . str_replace('-', '', ucwords($class, '-'));
        if ( class_exists($injector_class) ) {
            if ( ! is_subclass_of( $injector_class, Injectors\AbstractInjector::class ) ) {
                throw new \Exception("The injector class must extend '" . Injectors\AbstractInjector::class . "'.");
            }
            $injector = sage()->makeWith($injector_class, ['data' => $data]);
            $injector->run();
            $data = $injector->getAll();
        }
        // Apply Sage's default filters:
        $template = apply_filters("sage/template/{$class}/data", $data, $template);
        // Return the template
        return $template;
    }, []);
    if ($template) {
        echo template($template, $data);
        return get_stylesheet_directory().'/index.php';
    }
    return $template;
}, PHP_INT_MAX);

Create the AbstractInjector.php class inside app/Injectors directory:

<?php // app/Injectors/AbstractInjector.php

namespace App\Injectors;

abstract class AbstractInjector
{
    protected $data = [];

    /**
     * Create a new AbstractInjector
     *
     * @param array $data
     */
    public function __construct( array $data = [] )
    {
        $this->data = $data;
    }  

    /**
     * Run the AbstractInjector
     *
     * @return void
     */
    abstract public function run();

    /**
     * Set a data key => value
     *
     * @param string $key
     * @param mixed $val
     * @return void
     */
    protected function set( string $key, $val )
    {
        $this->data[ $key ] = $val;
    }

    /**
     * Get a single data value
     *
     * @param string $key
     * @return mixed
     */
    public function get( string $key )
    {
        return isset( $this->data[ $key ] ) ? $this->data[ $key ] : null;
    }

    /**
     * Get all data
     *
     * @return array
     */
    public function getAll(): array
    {
        return $this->data;
    }
}

Create your data Injector classes (also inside app/Injectors directory):

i.e. where Single provides data to the resources/views/single.blade.php template (snake-case to CamelCase, as with controllers).

<?php // app/Injectors/Single.php

namespace App\Injectors;

use App\Injectors\AbstractInjector;

class Single extends AbstractInjector
{
    public function __construct()
    {
        // We can also utilise the container's dependency injection here...
    }

    public function run()
    {
        // Set your view data...
        $this->set( 'header_image', get_field('img') );
        $this->set( 'text', 'Lorem Ipsum Dolor Sit Amet' );
    }
}

Here’s hoping the next Sage iteration has a slightly more stable Controller included.

Yes, the next iteration of Sage will be using View Composers, which are a feature of the template engine.

Awesome :slight_smile:

I just found the Sage 10 PR - what’s the vision for Acorn? Sounds like it’ll form the foundations of Sage going forward, but will also function as a framework for other plugins?

A well adopted WP plugin framework has been a long time coming, would be great to see one that becomes as popular as the other Roots projects.

1 Like

Feel free to watch Acorn progress here: https://github.com/qwp6t/acorn

At its core, Acorn is basically an application container that is semi-compatible with Laravel. I’ve successfully loaded numerous Laravel ServiceProviders into it without having to make any modifications, and other ServiceProviders have been loaded with only minor tweaking. The initial idea of Acorn was to allow Sage and plugins to both use Blade without tripping over each other. By sharing a single application container, we can ensure they’re all loading the same version of Blade. That’s still mostly what it is, except that instead of just sharing Blade, it’s going to allow any service provider to be registered and booted.

4 Likes

Just anecdotally, I’ve been using the current version of Acorn in a project, and it’s been really great to work with.

2 Likes

This topic was automatically closed after 42 days. New replies are no longer allowed.