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
99 changes: 98 additions & 1 deletion WebFiori/Framework/App.php
Original file line number Diff line number Diff line change
Expand Up @@ -582,7 +582,13 @@ private static function autoRegisterHelper($options) {
try {
$reflectionClass = new ReflectionClass($class);

$toPass = [$reflectionClass->newInstanceArgs($constructorParams)];
if (self::canAcceptArgs($reflectionClass, $constructorParams)) {
$instance = $reflectionClass->newInstanceArgs($constructorParams);
} else {
$instance = $reflectionClass->newInstance();
}

$toPass = [$instance];

foreach ($otherParams as $param) {
$toPass[] = $param;
Expand All @@ -591,6 +597,97 @@ private static function autoRegisterHelper($options) {
} catch (Error $ex) {
}
}
/**
* Checks if a class constructor can accept the given arguments.
*
* Returns true if the constructor can be called with the given params.
* Returns false if there is a type mismatch or if the constructor
* has no parameters but args were provided.
*
* @param ReflectionClass $refClass The reflection of the class to check.
* @param array $args The arguments to check against the constructor.
*
* @return bool
*/
private static function canAcceptArgs(ReflectionClass $refClass, array $args): bool {
if (empty($args)) {
return true;
}

$constructor = $refClass->getConstructor();

if ($constructor === null) {
return false;
}

$params = $constructor->getParameters();

if (count($args) > count($params)) {
return false;
}

foreach ($args as $index => $arg) {
if (!isset($params[$index])) {
return false;
}

$paramType = $params[$index]->getType();

if ($paramType === null) {
// No type hint, accepts anything.
continue;
}

if ($paramType instanceof \ReflectionUnionType) {
$matched = false;

foreach ($paramType->getTypes() as $type) {
if (self::argMatchesType($arg, $type)) {
$matched = true;
break;
}
}

if (!$matched) {
return false;
}
} else if (!self::argMatchesType($arg, $paramType)) {
return false;
}
}

return true;
}
/**
* Checks if a single argument matches a single reflected type.
*
* @param mixed $arg The argument value.
* @param \ReflectionNamedType $type The reflected type to check against.
*
* @return bool
*/
private static function argMatchesType($arg, \ReflectionNamedType $type): bool {
$typeName = $type->getName();

if ($arg === null) {
return $type->allowsNull();
}

if ($type->isBuiltin()) {
return match ($typeName) {
'string' => is_string($arg),
'int' => is_int($arg),
'float' => is_float($arg) || is_int($arg),
'bool' => is_bool($arg),
'array' => is_array($arg),
'callable' => is_callable($arg),
'mixed' => true,
default => false,
};
}

return $arg instanceof $typeName;
}
/**
* Safe function caller with CLI/web-aware exception handling.
*
Expand Down
60 changes: 60 additions & 0 deletions tests/WebFiori/Framework/Tests/AutoRegisterTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
<?php
namespace WebFiori\Framework\Test;

use PHPUnit\Framework\TestCase;
use WebFiori\Framework\App;
use WebFiori\Http\AbstractWebService;

class AutoRegisterTest extends TestCase {
/**
* @test
* Test that attribute-based services (using #[RestController]) are
* correctly discovered and registered by autoRegister().
* This is the fix for issue #313.
*/
public function testAutoRegisterAttributedService() {
$registered = [];
App::autoRegister('Apis/AttributedTest', function (AbstractWebService $ws) use (&$registered)
{
$registered[] = $ws;
}, 'Service');

$this->assertCount(1, $registered, 'Expected one attributed service to be registered.');
$this->assertEquals('attributed-service', $registered[0]->getName());
$this->assertEquals('A service registered via attribute', $registered[0]->getDescription());
}

/**
* @test
* Reproduces the exact bug from issue #313: registerServices() passes
* [$this] (a manager object) as constructor params. For attribute-based
* services, the constructor expects string, not an object. The fix should
* detect the type mismatch and instantiate with no args instead.
*/
public function testAutoRegisterAttributedServiceWithIncompatibleConstructorArgs() {
$managerMock = new \stdClass();
$registered = [];

App::autoRegister('Apis/AttributedTest2', function (AbstractWebService $ws) use (&$registered)
{
$registered[] = $ws;
}, 'Service', [$managerMock]);

$this->assertCount(1, $registered, 'Attributed service should be registered even when incompatible constructor args are passed.');
$this->assertEquals('attributed-service-2', $registered[0]->getName());
}

/**
* @test
* Test that constructor-based services still work when suffix matches.
*/
public function testAutoRegisterConstructorBasedServiceWithSuffix() {
$registered = [];
App::autoRegister('tests/Apis/Multiple', function (AbstractWebService $ws) use (&$registered)
{
$registered[] = $ws;
}, 'Service');

$this->assertCount(0, $registered);
}
}
Loading