Skip to content

View

Foundation View provides a small contract for rendering named views and a default PhpView implementation that renders trusted PHP templates from an explicit directory. It keeps markup in view files while application services remain responsible for selecting the view and preparing its data.

PhpView uses ordinary PHP templates without introducing custom template syntax. It captures their output and returns it as a string instead of echoing it automatically. A configured renderer can also create an immutable renderer for another trusted directory at runtime.

Install the split package:

composer require stellarwp/foundation-view

Foundation View uses the shared application configuration and provider architecture established in these guides:

Create a views/ directory at the application root. In the root config.php, provide its absolute path:

<?php declare(strict_types=1);

return [
	'view' => [
		'directory' => __DIR__ . '/views',
	],
];

view.directory is required and must identify an existing, readable directory. Foundation resolves it to its canonical path before rendering.

In src/App.php, add ViewProvider before feature providers that consume the View contract:

use StellarWP\Foundation\Container\Contracts\Providable;
use StellarWP\Foundation\View\ViewProvider;
use YourPlugin\Admin;

/** @var list<class-string<Providable>> */
private const array PROVIDERS = [
	ViewProvider::class,
	Admin\Provider::class,
];

ViewProvider binds one shared PhpView instance to StellarWP\Foundation\View\Contracts\View and StellarWP\Foundation\View\Contracts\DirectoryAwareView.

View names are relative to the configured directory and omit the .php extension. For the name admin/product-summary, first create views/admin/product-summary.php:

<?php
/**
 * @var string $title   The notice heading.
 * @var string $summary The product availability summary.
 */
?>
<div class="notice notice-info">
	<p><strong><?php echo esc_html( $title ); ?></strong></p>
	<p><?php echo esc_html( $summary ); ?></p>
</div>

In src/Admin/Product_Summary_Notice.php, inject the View contract and return or echo the rendered string at the application boundary:

<?php declare(strict_types=1);

namespace YourPlugin\Admin;

use StellarWP\Foundation\View\Contracts\View;

/**
 * Displays the current product count in WordPress administration.
 */
final readonly class Product_Summary_Notice {

	public function __construct(
		private View $view,
		private Product_Repository $products
	) {
	}

	/**
	 * @action admin_notices
	 */
	public function display(): void {
		$count = $this->products->count();

		echo $this->view->render(
			'admin/product-summary',
			[
				'title'   => __( 'Product catalog', 'your-plugin' ),
				'summary' => sprintf(
					/* translators: %d: number of products. */
					_n( '%d product is available.', '%d products are available.', $count, 'your-plugin' ),
					$count
				),
			]
		);
	}
}

Register the WordPress callback from src/Admin/Provider.php:

<?php declare(strict_types=1);

namespace YourPlugin\Admin;

use StellarWP\Foundation\Container\Contracts\Provider as Service_Provider;

/**
 * Registers administration services and hooks.
 */
final class Provider extends Service_Provider {

	public function register(): void {
		add_action(
			'admin_notices',
			$this->container->callback( Product_Summary_Notice::class, 'display' )
		);
	}
}

Inject DirectoryAwareView instead of the base View contract when a service must select another trusted template root, such as a theme override directory:

<?php declare(strict_types=1);

namespace YourPlugin\Receipt;

use StellarWP\Foundation\View\Contracts\DirectoryAwareView;

final readonly class Receipt_Renderer {

	public function __construct(
		private DirectoryAwareView $view
	) {
	}

	public function render( string $trusted_template_directory, Receipt $receipt ): string {
		$renderer = $this->view->withDirectory( $trusted_template_directory );

		return $renderer->render(
			'email/receipt',
			[ 'receipt' => $receipt ]
		);
	}
}

withDirectory() returns a new renderer. It does not mutate the shared renderer or affect other services using the configured directory.

The base View contract requires only named rendering. A renderer that does not use PHP files or directories can implement it without supporting withDirectory():

<?php declare(strict_types=1);

namespace YourPlugin\View;

use JsonException;
use StellarWP\Foundation\View\Contracts\View;

final class Json_View implements View {

	/**
	 * @throws JsonException When the supplied data cannot be encoded.
	 */
	public function render( string $name, array $data = [] ): string {
		return json_encode(
			[
				'view' => $name,
				'data' => $data,
			],
			JSON_THROW_ON_ERROR
		);
	}
}

Bind the replacement from the application’s feature provider instead of registering ViewProvider:

use StellarWP\Foundation\View\Contracts\View;

public function register(): void {
	$this->container->singleton( View::class, Json_View::class );
}

Use a separate capability contract when a custom renderer supports optional behavior such as runtime directory selection. Application services that only call render() should continue depending on View.

The renderer throws ViewNotFoundException when a view is missing, unreadable, or resolves outside the selected directory. Empty names, absolute paths, null bytes, and parent traversal such as ../private are rejected with InvalidArgumentException.

Exceptions thrown by the view itself are propagated after Foundation removes any removable buffers opened while rendering. A view may use balanced buffers of its own, but it must not clean, flush, close, or replace Foundation’s rendering buffer. Invalid buffer state is rejected instead of returning incomplete output. Let application-level error handling record or present those failures rather than returning a partial template.

Place small PHP view fixtures under the test data directory. For example, create tests/_data/views/message.php:

<?php
/** @var string $message */
?><p><?php echo htmlspecialchars( $message, ENT_QUOTES, 'UTF-8' ); ?></p>

Render the fixture with the concrete class:

use StellarWP\Foundation\View\PhpView;

$view = new PhpView( codecept_data_dir( 'views' ) );

$this->assertSame(
	'<p>Hello, Foundation</p>',
	$view->render(
		'message',
		[ 'message' => 'Hello, Foundation' ]
	)
);

Test feature services through the View contract when the rendered markup is part of their observable behavior. Use a temporary directory under tests/_data/temp for path-containment or runtime-directory tests that must create files.