Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
5 changes: 4 additions & 1 deletion src/Instrument/Transformer/WeavingTransformer.php
Original file line number Diff line number Diff line change
Expand Up @@ -976,7 +976,10 @@ private function processFunctions(
mkdir($dirname, $this->options['cacheFileMode'], true);
}
$generator = new FunctionProxyGenerator($namespace, $functionAdvices);
file_put_contents($functionFileName, $generator->generate(), LOCK_EX);
// PHP core refuses the LOCK_EX flag for any non-"file://" stream wrapper path,
// see saveProxyToCache() below.
$isStreamPath = str_contains($functionFileName, '://');
file_put_contents($functionFileName, $generator->generate(), $isStreamPath ? 0 : LOCK_EX);
// For cache files we don't want executable bits by default
chmod($functionFileName, $this->options['cacheFileMode'] & (~0111));
}
Expand Down
47 changes: 47 additions & 0 deletions tests/Aop/Framework/AbstractInvocationTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
<?php

declare(strict_types=1);
/*
* Go! AOP framework
*
* @copyright Copyright 2011, Lisachenko Alexander <lisachenko.it@gmail.com>
*
* This source file is subject to the license that is bundled
* with this source code in the file LICENSE.
*/

namespace Go\Aop\Framework;

use PHPUnit\Framework\TestCase;

class AbstractInvocationTest extends TestCase
{
private AbstractInvocation $invocation;

protected function setUp(): void
{
$this->invocation = new class ([]) extends AbstractInvocation {
public function proceed(): mixed
{
return null;
}

public function __toString(): string
{
return 'test-invocation';
}
};
}

public function testGetArgumentsReturnsEmptyArrayByDefault(): void
{
$this->assertSame([], $this->invocation->getArguments());
}

public function testSetArgumentsMutatesArguments(): void
{
$this->invocation->setArguments(['a', 42, true]);

$this->assertSame(['a', 42, true], $this->invocation->getArguments());
}
}
75 changes: 75 additions & 0 deletions tests/Aop/Framework/AbstractMethodInvocationTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -61,4 +61,79 @@ public function proceed(): string
$result = $o->proceed();
$this->assertEquals('testInstanceIsInitialized', $result);
}

public function testToStringForInstanceMethodUsesArrowNotation(): void
{
$o = new class extends AbstractMethodInvocation {
public function __construct()
{
parent::__construct([], AbstractMethodInvocationTest::class, 'testToStringForInstanceMethodUsesArrowNotation', static fn() => null);
}

public function isDynamic(): bool
{
return true;
}

public function getThis(): object
{
return $this;
}

public function getScope(): string
{
return self::class;
}

public function proceed(): mixed
{
return null;
}
};

$this->assertSame(
sprintf('execution(%s->testToStringForInstanceMethodUsesArrowNotation())', $o->getScope()),
(string) $o,
);
}

public function testToStringForStaticMethodUsesDoubleColonNotation(): void
{
$o = new class extends AbstractMethodInvocation {
public function __construct()
{
parent::__construct([], StaticHelperForAbstractMethodInvocationTest::class, 'staticMethod', static fn() => null);
}

public function isDynamic(): bool
{
return false;
}

public function getThis(): ?object
{
return null;
}

public function getScope(): string
{
return self::class;
}

public function proceed(): mixed
{
return null;
}
};

$this->assertSame(
sprintf('execution(%s::staticMethod())', $o->getScope()),
(string) $o,
);
}
}

class StaticHelperForAbstractMethodInvocationTest
{
public static function staticMethod(): void {}
}
96 changes: 96 additions & 0 deletions tests/Aop/Framework/ClassFieldAccessTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -36,4 +36,100 @@ public function testWriteInvocationWithoutBackedValueDoesNotFail(): void

$this->assertSame('updated', $result);
}

public function testReadInvocationWithBackedValueReturnsOriginalValue(): void
{
$originalValue = 'original';
$result = $this->classField->__invoke($this, FieldAccessType::READ, $originalValue);

$this->assertSame('original', $result);
$this->assertSame('original', $this->classField->getValue());
}

public function testGetAccessTypeReturnsTypeUsedDuringInvocation(): void
{
$value = 'foo';
$this->classField->__invoke($this, FieldAccessType::READ, $value);

$this->assertSame(FieldAccessType::READ, $this->classField->getAccessType());
}

public function testGetValueToSetReturnsNewValueForWriteAccess(): void
{
$newValue = 'updated-value';
$this->classField->__invoke($this, FieldAccessType::WRITE, $newValue);

$this->assertSame('updated-value', $this->classField->getValueToSet());
}

public function testGetValueToSetThrowsForReadAccessType(): void
{
$value = 'foo';
$this->classField->__invoke($this, FieldAccessType::READ, $value);

$this->expectException(\Go\Aop\AspectException::class);
$this->expectExceptionMessage('Value to set is not available for READ access type');
$this->classField->getValueToSet();
}

public function testGetThisReturnsBoundInstance(): void
{
$value = 'foo';
$this->classField->__invoke($this, FieldAccessType::READ, $value);

$this->assertSame($this, $this->classField->getThis());
}

public function testIsDynamicReturnsTrue(): void
{
// @phpstan-ignore method.alreadyNarrowedType (runtime double-check of the declared return type)
$this->assertTrue($this->classField->isDynamic());
}

public function testGetScopeReturnsClassOfBoundInstance(): void
{
$value = 'foo';
$this->classField->__invoke($this, FieldAccessType::READ, $value);

$this->assertSame(self::class, $this->classField->getScope());
}

public function testToStringDescribesReadAccess(): void
{
$value = 'foo';
$this->classField->__invoke($this, FieldAccessType::READ, $value);

$this->assertSame(
sprintf('get(%s->classField)', self::class),
(string) $this->classField,
);
}

public function testToStringDescribesWriteAccess(): void
{
$newValue = 'foo';
$this->classField->__invoke($this, FieldAccessType::WRITE, $newValue);

$this->assertSame(
sprintf('set(%s->classField)', self::class),
(string) $this->classField,
);
}

public function testProceedInvokesInterceptorChainBeforeReturningPropertyValue(): void
{
$calls = [];
$advice = new AroundInterceptor(function (\Go\Aop\Intercept\FieldAccess $fieldAccess) use (&$calls): mixed {
$calls[] = $fieldAccess->getAccessType();

return $fieldAccess->proceed();
});

$classField = new ClassFieldAccess([$advice], self::class, 'classField');
$value = 'intercepted';
$result = $classField->__invoke($this, FieldAccessType::READ, $value);

$this->assertSame('intercepted', $result);
$this->assertSame([FieldAccessType::READ], $calls);
}
}
93 changes: 93 additions & 0 deletions tests/Aop/Framework/InterceptorInjectorTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
<?php

declare(strict_types=1);
/*
* Go! AOP framework
*
* @copyright Copyright 2011, Lisachenko Alexander <lisachenko.it@gmail.com>
*
* This source file is subject to the license that is bundled
* with this source code in the file LICENSE.
*/

namespace Go\Aop\Framework;

use Go\Aop\Intercept\FieldAccessType;
use Go\Aop\Intercept\Interceptor as InterceptorContract;
use Go\Stubs\TraitAliasProxy;
use PHPUnit\Framework\TestCase;

class InterceptorInjectorTest extends TestCase
{
protected string $classProperty;

/**
* @return non-empty-list<InterceptorContract>
*/
private function noopInterceptors(): array
{
return [Interceptor::before(static fn() => null)];
}

public function testForMethodBuildsDynamicTraitAliasMethodInvocation(): void
{
$instance = new TraitAliasProxy();
$callable = $instance->getCallableFor('publicMethod');
$invocation = InterceptorInjector::forMethod(TraitAliasProxy::class, 'publicMethod', $this->noopInterceptors(), $callable);

$this->assertInstanceOf(DynamicTraitAliasMethodInvocation::class, $invocation);
$result = $invocation($instance);
$this->assertSame(T_PUBLIC, $result);
}

public function testForStaticMethodBuildsStaticTraitAliasMethodInvocation(): void
{
$callable = TraitAliasProxy::getStaticCallableFor('staticPublicMethod');
$invocation = InterceptorInjector::forStaticMethod(TraitAliasProxy::class, 'staticPublicMethod', $this->noopInterceptors(), $callable);

$this->assertInstanceOf(StaticTraitAliasMethodInvocation::class, $invocation);
$result = $invocation(TraitAliasProxy::class);
$this->assertSame(TraitAliasProxy::class, $result);
}

public function testForPropertyBuildsClassFieldAccess(): void
{
$fieldAccess = InterceptorInjector::forProperty(self::class, 'classProperty', $this->noopInterceptors());

$this->assertInstanceOf(ClassFieldAccess::class, $fieldAccess);
$this->assertSame('classProperty', $fieldAccess->getField()->name);

$value = 'hello';
$result = $fieldAccess->__invoke($this, FieldAccessType::READ, $value);
$this->assertSame('hello', $result);
}

public function testForFunctionBuildsReflectionFunctionInvocation(): void
{
$invocation = InterceptorInjector::forFunction('strlen', $this->noopInterceptors(), \strlen(...));

$this->assertInstanceOf(ReflectionFunctionInvocation::class, $invocation);
$this->assertSame(5, $invocation(['hello']));
}

public function testForStaticInitializationBuildsStaticInitializationJoinpoint(): void
{
$called = false;
$interceptors = [Interceptor::before(static function () use (&$called): void {
$called = true;
})];
$joinPoint = InterceptorInjector::forStaticInitialization(self::class, $interceptors);

$this->assertInstanceOf(StaticInitializationJoinpoint::class, $joinPoint);
$joinPoint();
$this->assertTrue($called);
}

public function testForInitializationBuildsReflectionConstructorInvocation(): void
{
$invocation = InterceptorInjector::forInitialization(self::class, $this->noopInterceptors());

$this->assertInstanceOf(ReflectionConstructorInvocation::class, $invocation);
$this->assertSame(self::class, $invocation->getScope());
}
}
47 changes: 47 additions & 0 deletions tests/Aop/Framework/ReflectionFunctionInvocationTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -100,4 +100,51 @@ public function testPassByReferenceIsForwarded(): void
// @phpstan-ignore method.impossibleType ($matches is filled by reference inside the invocation)
$this->assertSame(['123', '123'], $matches);
}

/**
* getFunction() exposes the underlying ReflectionFunction instance.
*/
public function testGetFunctionReturnsReflectionFunction(): void
{
$invocation = new ReflectionFunctionInvocation([], 'strlen', \strlen(...));

$this->assertSame('strlen', $invocation->getFunction()->getName());
}

/**
* __toString() produces a friendly `execution(functionName())` description.
*/
public function testToStringDescribesFunctionExecution(): void
{
$invocation = new ReflectionFunctionInvocation([], 'strlen', \strlen(...));

$this->assertSame('execution(strlen())', (string) $invocation);
}

/**
* Recursive invocations (the callable calling the same joinpoint again) must push
* the current arguments/cursor onto a stack and restore them once the nested call
* unwinds, so the outer call resumes with its own arguments intact.
*/
public function testRecursiveInvocationPreservesOuterStackFrame(): void
{
$invocation = null;
$callable = static function (int $n) use (&$invocation): int {
if ($n <= 0) {
return 0;
}

/** @var ReflectionFunctionInvocation $invocation */
$nested = $invocation([$n - 1]);
// @phpstan-ignore cast.int (FunctionInvocation's generic V is unresolved here; the wrapped callable is declared `: int`)
return $n + (int) $nested;
};
$invocation = new ReflectionFunctionInvocation([], 'strlen', $callable);

$result = $invocation([3]);

$this->assertSame(6, $result);
// Outer call's arguments must be restored after the nested recursive calls unwind.
$this->assertSame([3], $invocation->getArguments());
}
}
Loading
Loading