Skip to content

Commit

Permalink
allow to access drop public properties with camel or snake case name
Browse files Browse the repository at this point in the history
  • Loading branch information
cappuc committed Apr 4, 2024
1 parent 69381a3 commit 0d316e3
Show file tree
Hide file tree
Showing 6 changed files with 81 additions and 35 deletions.
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -94,11 +94,11 @@ $stream = $template->stream($context);
Liquid support almost any kind of object but in order to have a better control over the accessible data in the templates,
you can pass your data as `Drop` objects and have a better control over the accessible data.
Drops are standard php objects that extend the `Keepsuit\Liquid\Drop` class.
Each public method of the class will be accessible in the template as a property.
Public properties and public methods of the class will be accessible in the template as a property.
You can also override the `liquidMethodMissing` method to handle undefined properties.

Liquid provides some attributes to control the behavior of the drops:
- `Hidden`: Hide the method from the template, it cannot be accessed as a property.
- `Hidden`: Hide the method or the property from the template, it cannot be accessed from liquid.
- `Cache`: Cache the result of the method, it will be called only once and the result will be stored in the drop.

```php
Expand Down
2 changes: 1 addition & 1 deletion src/Attributes/Hidden.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
* This attribute can be used to mark a drop method as hidden,
* so it won't be exposed to the liquid context.
*/
#[Attribute(Attribute::TARGET_METHOD)]
#[Attribute(Attribute::TARGET_METHOD | Attribute::TARGET_PROPERTY)]
class Hidden
{
}
12 changes: 9 additions & 3 deletions src/Drop.php
Original file line number Diff line number Diff line change
Expand Up @@ -31,11 +31,17 @@ public function __get(string $name): mixed
$invokableMethods = $this->getMetadata()->invokableMethods;
$cacheableMethods = $this->getMetadata()->cacheableMethods;

$possibleNames = [
$possibleNames = array_unique([
$name,
Str::camel($name),
Str::snake($name),
];
]);

foreach ($possibleNames as $propertyName) {
if (in_array($propertyName, $this->getMetadata()->properties)) {
return $this->{$propertyName};
}
}

foreach ($possibleNames as $methodName) {
if (! in_array($methodName, $invokableMethods)) {
Expand Down Expand Up @@ -66,7 +72,7 @@ public function __get(string $name): mixed
}
}

if ($this->context->strictVariables) {
if (isset($this->context) && $this->context->strictVariables) {
throw new UndefinedDropMethodException($name);
}

Expand Down
57 changes: 38 additions & 19 deletions src/Support/DropMetadata.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,26 @@
use Keepsuit\Liquid\Drop;
use ReflectionClass;
use ReflectionMethod;
use ReflectionProperty;
use Traversable;

/**
* @internal
*/
final class DropMetadata
{
/**
* @var array<string,mixed>
*/
protected static array $cache = [];

public function __construct(
public readonly array $invokableMethods = [],
public readonly array $cacheableMethods = [],
public readonly array $properties = [],
) {
}

public static function init(Drop $drop): DropMetadata
{
if (isset(self::$cache[get_class($drop)])) {
Expand All @@ -31,32 +42,40 @@ public static function init(Drop $drop): DropMetadata
$blacklist = [...$blacklist, 'current', 'next', 'key', 'valid', 'rewind'];
}

$publicMethods = (new ReflectionClass($drop))->getMethods(ReflectionMethod::IS_PUBLIC);
$classReflection = new ReflectionClass($drop);

$visibleMethodNames = array_map(
fn (ReflectionMethod $method) => $method->getAttributes(Hidden::class) !== [] ? null : $method->getName(),
$publicMethods = array_filter(
$classReflection->getMethods(ReflectionMethod::IS_PUBLIC),
fn (ReflectionMethod $method) => $method->getAttributes(Hidden::class) === []
&& ! in_array($method->getName(), $blacklist)
&& ! str_starts_with($method->getName(), '__')
);

$invokableMethods = array_map(
fn (ReflectionMethod $method) => $method->getName(),
$publicMethods
);

$invokableMethods = array_values(array_filter(
array_diff($visibleMethodNames, $blacklist),
fn (?string $name) => $name !== null && ! str_starts_with($name, '__')
));
$cacheableMethods = array_map(
fn (ReflectionMethod $method) => $method->getName(),
array_filter(
$publicMethods,
fn (ReflectionMethod $method) => $method->getAttributes(Cache::class) !== []
)
);

$cacheableMethods = array_values(array_filter(array_map(
fn (ReflectionMethod $method) => $method->getAttributes(Cache::class) !== [] ? $method->getName() : null,
$publicMethods
)));
$publicProperties = array_map(
fn (ReflectionProperty $property) => $property->getName(),
array_filter(
$classReflection->getProperties(ReflectionProperty::IS_PUBLIC),
fn (ReflectionProperty $property) => $property->getAttributes(Hidden::class) === []
)
);

return self::$cache[get_class($drop)] = new DropMetadata(
invokableMethods: $invokableMethods,
cacheableMethods: $cacheableMethods
invokableMethods: array_values($invokableMethods),
cacheableMethods: array_values($cacheableMethods),
properties: array_values($publicProperties)
);
}

public function __construct(
public readonly array $invokableMethods,
public readonly array $cacheableMethods
) {
}
}
32 changes: 24 additions & 8 deletions tests/Integration/DropTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

use Keepsuit\Liquid\Render\RenderContext;
use Keepsuit\Liquid\Tests\Stubs\CachableDrop;
use Keepsuit\Liquid\Tests\Stubs\CatchAllDrop;
use Keepsuit\Liquid\Tests\Stubs\ContextDrop;
use Keepsuit\Liquid\Tests\Stubs\EnumerableDrop;
use Keepsuit\Liquid\Tests\Stubs\ProductDrop;
Expand All @@ -20,19 +21,19 @@
});

test('text drop', function () {
expect(renderTemplate(' {{ product.texts.text }} ', ['product' => new ProductDrop()]))->toBe(' text1 ');
expect(renderTemplate(' {{ product.text.text }} ', ['product' => new ProductDrop()]))->toBe(' text1 ');
});

test('catchall unknown method', function () {
expect(renderTemplate(' {{ product.catchall.unknown }} ', ['product' => new ProductDrop()]))->toBe(' catchall_method: unknown ');
expect(renderTemplate(' {{ product.catch_all.unknown }} ', ['product' => new ProductDrop()]))->toBe(' catchall_method: unknown ');
});

test('catchall integer argument drop', function () {
expect(renderTemplate(' {{ product.catchall[8] }} ', ['product' => new ProductDrop()]))->toBe(' catchall_method: 8 ');
expect(renderTemplate(' {{ product.catch_all[8] }} ', ['product' => new ProductDrop()]))->toBe(' catchall_method: 8 ');
});

test('text array drop', function () {
expect(renderTemplate('{% for text in product.texts.array %} {{text}} {% endfor %}', ['product' => new ProductDrop()]))->toBe(' text1 text2 ');
expect(renderTemplate('{% for text in product.text.array %} {{text}} {% endfor %}', ['product' => new ProductDrop()]))->toBe(' text1 text2 ');
});

test('context drop', function () {
Expand Down Expand Up @@ -108,16 +109,19 @@

test('drop metadata', function () {
expect(invade(new ProductDrop())->getMetadata())
->invokableMethods->toBe(['texts', 'catchall', 'context'])
->cacheableMethods->toBe([]);
->invokableMethods->toBe(['text', 'catchAll', 'context'])
->cacheableMethods->toBe([])
->properties->toBe(['productName']);

expect(invade(new EnumerableDrop())->getMetadata())
->invokableMethods->toBe(['size', 'first', 'count', 'min', 'max'])
->cacheableMethods->toBe([]);
->cacheableMethods->toBe([])
->properties->toBe([]);

expect(invade(new CachableDrop())->getMetadata())
->invokableMethods->toBe(['notCached', 'cached'])
->cacheableMethods->toBe(['cached']);
->cacheableMethods->toBe(['cached'])
->properties->toBe([]);
});

it('can cache drop method calls', function () {
Expand All @@ -131,3 +135,15 @@
->cached->toBe(0)
->cached->toBe(0);
});

it('can access drop data with snake and camel cases', function () {
$drop = new ProductDrop();

expect($drop)
->productName->toBe('Product')
->product_name->toBe('Product');

expect($drop)
->catchAll->toBeInstanceOf(CatchAllDrop::class)
->catch_all->toBeInstanceOf(CatchAllDrop::class);
});
9 changes: 7 additions & 2 deletions tests/Stubs/ProductDrop.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,17 @@

class ProductDrop extends Drop
{
public function texts(): TextDrop
public function __construct(
public string $productName = 'Product'
) {
}

public function text(): TextDrop
{
return new TextDrop();
}

public function catchall(): CatchAllDrop
public function catchAll(): CatchAllDrop
{
return new CatchAllDrop();
}
Expand Down

0 comments on commit 0d316e3

Please sign in to comment.