Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
784715d
Implement native dependency injection for template controllers
May 18, 2026
8ad3e00
Refine template controller DI and add comprehensive test suite
May 18, 2026
659057c
Apply phpcs fixes
May 18, 2026
c4cc725
Optimize DI resolvers and clean up service provider following review
May 18, 2026
352ad2d
Optimize DI resolvers and clean up service provider following Orbit r…
May 18, 2026
9e9721a
Refactor DI system to Template Method pattern and consolidate tests
May 18, 2026
ddcd3de
Refactor DI system to Template Method pattern and consolidate tests
May 18, 2026
2a261fc
Address final review suggestions for DI refactor
May 18, 2026
b1a96e1
Merge branch 'feature/template-controller-di' of github.com:Rareloop/…
May 18, 2026
accad99
Re-add comment
May 18, 2026
54b92b1
Move core resolvers
May 18, 2026
6a9a14f
Build controllers with DI
May 19, 2026
4cdf339
Ensure that Timber context has array access methods
May 19, 2026
63d2560
Add static class resolution parity for Term and User proxy objects
May 19, 2026
fbe44c0
Simplify if statements
May 19, 2026
bff4f3b
Remove unused import
May 19, 2026
9c27db8
Remove from gitignore
May 19, 2026
344e757
Remove reflection
May 19, 2026
d9ee4c7
Revert controller change
May 19, 2026
16158d5
Remove TimberResponse Bind
May 19, 2026
f24b6e7
Add `isValidContext` methods
May 19, 2026
c1a77e4
Remove ControllerTest
May 19, 2026
be3ebbb
Update Timber import
May 20, 2026
fbf376c
Change getCoreResolvers visibility
May 20, 2026
ec23d81
Remove toCollection from PostQuery
May 20, 2026
1858617
Apply fixes from comments
May 20, 2026
e1bf2ef
Remove PostTypeResolver
May 20, 2026
b234850
Remove unused import
May 20, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/Application.php
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,7 @@ public function register($provider)
}

if (is_string($provider)) {
$provider = new $provider($this);
$provider = $this->make($provider);
}

if (method_exists($provider, 'register')) {
Expand Down
4 changes: 4 additions & 0 deletions src/Bootstrappers/RegisterRequestHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,5 +13,9 @@ public function bootstrap(Application $app)
if ($config->get('app.debug')) {
$app->detectWhenRequestHasNotBeenHandled();
}

$app->bind(\WP_Query::class, function () {
return $GLOBALS['wp_query'];
});
}
}
16 changes: 16 additions & 0 deletions src/Exceptions/MismatchedContextException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<?php

namespace Rareloop\Lumberjack\Exceptions;

class MismatchedContextException extends UnresolvableContextException
{
public static function forIncorrectClass(string $expectedClass, mixed $actualValue): self
{
$actualType = is_object($actualValue) ? $actualValue::class : gettype($actualValue);

return new static(
"Resolved a WordPress object, but it was of type [{$actualType}] " .
"instead of the expected [{$expectedClass}]."
);
}
}
16 changes: 16 additions & 0 deletions src/Exceptions/MissingContextException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<?php

namespace Rareloop\Lumberjack\Exceptions;

class MissingContextException extends UnresolvableContextException
{
public static function forType(string $expectedClass, mixed $actualObject): self
{
$actualType = get_debug_type($actualObject);

return new static(
"Could not resolve context for typehint [{$expectedClass}]. " .
"The current WordPress queried object is [{$actualType}]."
);
}
}
9 changes: 9 additions & 0 deletions src/Exceptions/UnresolvableContextException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<?php

namespace Rareloop\Lumberjack\Exceptions;

use Exception;

class UnresolvableContextException extends Exception
{
}
7 changes: 6 additions & 1 deletion src/Helpers.php
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,12 @@ public static function config($key, $default = null)

public static function view($template, $context = [], $statusCode = 200, $headers = [])
{
return new TimberResponse($template, $context, $statusCode, $headers);
return static::app()->make(TimberResponse::class, [
'twigTemplate' => $template,
'context' => $context,
'status' => $statusCode,
'headers' => $headers,
]);
}

public static function route($name, $params = [])
Expand Down
102 changes: 102 additions & 0 deletions src/Http/Resolvers/AbstractContextResolver.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
<?php

namespace Rareloop\Lumberjack\Http\Resolvers;

use Illuminate\Support\Arr;
use Invoker\ParameterResolver\ParameterResolver;
use Rareloop\Lumberjack\Exceptions\MismatchedContextException;
use Rareloop\Lumberjack\Exceptions\MissingContextException;
use ReflectionFunctionAbstract;

abstract class AbstractContextResolver implements ParameterResolver
{
public function getParameters(
ReflectionFunctionAbstract $reflection,
array $providedParameters,
array $resolvedParameters
): array {
foreach ($reflection->getParameters() as $parameter) {
if (Arr::has($resolvedParameters, $parameter->getPosition())) {
continue;
}

$type = $parameter->getType();

if (!$type || $type->isBuiltin()) {
continue;
}

$className = $type->getName();

if (!$this->canResolveClass($className)) {
continue;
}

try {
$context = $this->getContext();

if (is_null($context)) {
throw MissingContextException::forType($className, $context);
}

if (!$this->isValidContext($context, $className)) {
throw MismatchedContextException::forIncorrectClass($className, $context);
}

$resolvedObject = $this->resolveObject($className, $context);

if (!is_null($resolvedObject) && !$resolvedObject instanceof $className) {
throw MismatchedContextException::forIncorrectClass($className, $resolvedObject);
}

$resolvedParameters[$parameter->getPosition()] = $resolvedObject;
} catch (MissingContextException | MismatchedContextException $e) {
Comment thread
AliceKLWilliams marked this conversation as resolved.
// If the context is entirely missing or mismatched, we allow null if the typehint supports it
if (!$parameter->allowsNull()) {
throw $e;
}

$resolvedParameters[$parameter->getPosition()] = null;
}
}

return $resolvedParameters;
}

/**
* Get the raw context object to resolve from (e.g. WP_Post, WP_Term, WP_Query).
* Defaults to the current WordPress queried object.
*
* @return mixed
*/
protected function getContext(): mixed
{
return get_queried_object();
}

/**
* Determine if this resolver can handle the given class type-hint.
*
* @param string $className
* @return bool
*/
abstract protected function canResolveClass(string $className): bool;

/**
* Determine if the current context is valid for this resolver.
*
* @param mixed $context
* @param string $className
* @return bool
*/
abstract protected function isValidContext(mixed $context, string $className): bool;

/**
* Build the concrete object instance from the raw context.
*
* @param string $className
* @param mixed $context
* @return mixed
*/
abstract protected function resolveObject(string $className, mixed $context): mixed;
}
44 changes: 44 additions & 0 deletions src/Http/Resolvers/PostQueryResolver.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
<?php

namespace Rareloop\Lumberjack\Http\Resolvers;

use Rareloop\Lumberjack\Application;
use Timber\PostQuery as TimberPostQuery;
Comment thread
tommitchelmore marked this conversation as resolved.
use Timber\PostCollectionInterface;
use Timber\Timber;
use WP_Query;

class PostQueryResolver extends AbstractContextResolver
{
public function __construct(protected Application $app)
{
}

protected function canResolveClass(string $className): bool
{
return is_a($className, TimberPostQuery::class, true)
|| is_a($className, PostCollectionInterface::class, true);
}

protected function isValidContext(mixed $context, string $className): bool
{
return is_a($context, WP_Query::class);
}

protected function getContext(): mixed
{
return $this->app->get(WP_Query::class);
}

protected function resolveObject(string $className, mixed $context): mixed
{
// If they asked for the interface or the base Timber PostQuery, use the factory
if ($className === PostCollectionInterface::class || $className === TimberPostQuery::class) {
return Timber::get_posts($context);
}

// If it's a subclass (like Rareloop\Lumberjack\PostQuery), we must instantiate it manually
// to ensure we get the correct instance type.
return new $className($context);
}
}
28 changes: 28 additions & 0 deletions src/Http/Resolvers/PostResolver.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<?php

namespace Rareloop\Lumberjack\Http\Resolvers;

use Rareloop\Lumberjack\Post;
use Timber\Post as TimberPost;
use Timber\Timber;
use Timber\CoreEntityInterface;
use WP_Post;

class PostResolver extends AbstractContextResolver
{
protected function canResolveClass(string $className): bool
{
return is_a($className, Post::class, true)
|| is_a($className, TimberPost::class, true);
}

protected function isValidContext(mixed $context, string $className): bool
{
return is_a($context, WP_Post::class) || is_a($context, CoreEntityInterface::class);
}

protected function resolveObject(string $className, mixed $context): mixed
{
return Timber::get_post($context);
}
}
27 changes: 27 additions & 0 deletions src/Http/Resolvers/TermResolver.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<?php

namespace Rareloop\Lumberjack\Http\Resolvers;

use Rareloop\Lumberjack\Term;
use Timber\Term as TimberTerm;
use Timber\Timber;
use Timber\CoreEntityInterface;
use WP_Term;

class TermResolver extends AbstractContextResolver
{
protected function canResolveClass(string $className): bool
{
return is_a($className, Term::class, true) || is_a($className, TimberTerm::class, true);
}

protected function isValidContext(mixed $context, string $className): bool
{
return is_a($context, WP_Term::class) || is_a($context, CoreEntityInterface::class);
}

protected function resolveObject(string $className, mixed $context): mixed
{
return Timber::get_term($context);
}
}
27 changes: 27 additions & 0 deletions src/Http/Resolvers/UserResolver.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<?php

namespace Rareloop\Lumberjack\Http\Resolvers;

use Rareloop\Lumberjack\User;
use Timber\User as TimberUser;
use Timber\Timber;
use Timber\CoreEntityInterface;
use WP_User;

class UserResolver extends AbstractContextResolver
{
protected function canResolveClass(string $className): bool
{
return is_a($className, User::class, true) || is_a($className, TimberUser::class, true);
}

protected function isValidContext(mixed $context, string $className): bool
{
return is_a($context, WP_User::class) || is_a($context, CoreEntityInterface::class);
}

protected function resolveObject(string $className, mixed $context): mixed
{
return Timber::get_user($context);
}
}
8 changes: 5 additions & 3 deletions src/Http/Responses/TimberResponse.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,14 +21,16 @@ public function __construct($twigTemplate, $context, $status = 200, array $heade
parent::__construct($template, $status, $headers);
}

private function flattenContextToArrays(array $context): array
private function flattenContextToArrays(array|Arrayable|CollectionArrayable $context): array
{
$context = is_array($context) ? $context : $context->toArray();

// Recursively walk the array, when we find something that implements the Arrayable interface
// flatten it to an array. Because we're passing by reference by updating what the value of
// $item is will mutate the original data structure passed in.
array_walk_recursive($context, function (&$item, $key) {
array_walk_recursive($context, function (&$item) {
if ($item instanceof Arrayable || $item instanceof CollectionArrayable) {
$item = $this->flattenContextToArrays($item->toArray());
$item = $this->flattenContextToArrays($item);
}
});

Expand Down
Loading