Skip to content

Commit 5248360

Browse files
author
Yaroslav Zborovsky
committed
Initial setup
1 parent d007632 commit 5248360

File tree

8 files changed

+348
-2
lines changed

8 files changed

+348
-2
lines changed

.github/workflows/build.yml

+34
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
name: Build
2+
on: [push]
3+
4+
jobs:
5+
build:
6+
name: Run checks
7+
runs-on: ubuntu-latest
8+
steps:
9+
- name: Checkout
10+
uses: actions/checkout@v2
11+
12+
- name: Setup PHP
13+
uses: shivammathur/setup-php@v2
14+
with:
15+
php-version: '7.4'
16+
17+
- name: Get composer cache directory
18+
id: composer-cache
19+
run: echo "::set-output name=dir::$(composer config cache-files-dir)"
20+
21+
- name: Cache dependencies
22+
uses: actions/cache@v1
23+
with:
24+
path: ${{ steps.composer-cache.outputs.dir }}
25+
key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }}
26+
restore-keys: ${{ runner.os }}-composer-
27+
28+
- name: Install dependencies
29+
run: |
30+
composer install --prefer-dist
31+
32+
- name: Run tests
33+
run: composer test
34+

.gitignore

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
/.idea
2+
/vendor/
3+
/composer.lock

README.md

+64-2
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,64 @@
1-
# php-external-interface-matcher
2-
Simple reflection-based library to check if a class matches external interface
1+
# PHP implicit interface checker
2+
Simple reflection-based ~~library~~ function to check if a class matches an external interface (i.e. an interface
3+
that is not implemented by this class, but probably having the same method signatures).
4+
5+
## Wait, but why?
6+
This library was inspired by Go language and its concept of interfaces. In Go interfaces are
7+
implemented implicitly, i.e. an object implements an interface when it provides the same method signatures
8+
as described in the interface. Whereas in PHP interfaces MUST
9+
be implemented explicitly and this fact somehow prevents proper code decoupling.
10+
11+
Let's say we have the following class:
12+
13+
```php
14+
class DeepThought
15+
{
16+
public function answer(string $life, int $universe, array $everything): int
17+
{
18+
return 42;
19+
}
20+
}
21+
```
22+
23+
It does not implement any interface. Let us coin an interface for it:
24+
25+
```php
26+
interface DeepThoughtInterface
27+
{
28+
public function answer(string $life, int $universe, array $everything): int;
29+
}
30+
```
31+
32+
Actually, the signature of method `answer()` in the class completely matches the one in the interface.
33+
But we can't actually confirm that with any standard PHP tools.
34+
35+
```php
36+
$instance = new DeepThought();
37+
if ($instance instanceof DeepThoughtInterface::class) {
38+
echo 'Success';
39+
} else {
40+
echo 'Failure';
41+
}
42+
```
43+
44+
We will always receive `Failure` message. And this is sad.
45+
But now we can use this library for checking:
46+
47+
```php
48+
use function yaronius\ExternalInterfaceMatcher\class_provides;
49+
50+
$instance = new DeepThought();
51+
if (class_provides($instance, DeepThoughtInterface::class)) {
52+
echo 'Success';
53+
} else {
54+
echo 'Failure';
55+
}
56+
```
57+
58+
Now we get `Success`! Finally!
59+
60+
## TODO
61+
62+
- more OOP
63+
- more tests
64+
- benchmark performance impact

composer.json

+25
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
{
2+
"name": "yaronius/php-implicit-interface-checker",
3+
"require-dev": {
4+
"phpunit/phpunit": "^9.1"
5+
},
6+
"license": "MIT",
7+
"authors": [
8+
{
9+
"name": "Yaroslav Zborovsky",
10+
"email": "[email protected]"
11+
}
12+
],
13+
"autoload": {
14+
"files": [
15+
"src/functions.php"
16+
],
17+
"psr-4": {
18+
"yaronius\\ImplicitInterface\\": "src/"
19+
}
20+
},
21+
"scripts": {
22+
"test": "vendor/bin/phpunit"
23+
},
24+
"require": {}
25+
}

phpunit.xml

+15
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
<?xml version="1.0" encoding="utf-8"?>
2+
<phpunit bootstrap="./tests/bootstrap.php"
3+
backupGlobals="true"
4+
colors="true"
5+
convertErrorsToExceptions="true"
6+
convertNoticesToExceptions="true"
7+
convertWarningsToExceptions="true"
8+
stopOnFailure="false"
9+
>
10+
<testsuites>
11+
<testsuite name="Test Suite">
12+
<directory>./tests</directory>
13+
</testsuite>
14+
</testsuites>
15+
</phpunit>

src/functions.php

+145
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
<?php
2+
3+
namespace yaronius\ImplicitInterface;
4+
5+
use InvalidArgumentException;
6+
use ReflectionClass;
7+
use ReflectionMethod;
8+
use ReflectionParameter;
9+
use ReflectionException;
10+
use ReflectionNamedType;
11+
12+
/**
13+
* @param object|string $class
14+
* @param string $interface
15+
* @return bool
16+
* @throws ReflectionException
17+
*/
18+
function class_provides($class, string $interface): bool {
19+
// an object may be provided, so we handle this case here
20+
if (is_object($class)) {
21+
if ($class instanceof $interface) {
22+
return true;
23+
}
24+
$class = get_class($class);
25+
}
26+
27+
try {
28+
$classReflect = new ReflectionClass($class);
29+
} catch (ReflectionException $e) {
30+
//todo throw dedicated exception
31+
return false;
32+
}
33+
34+
// check if the class explicitly implements the interface
35+
if ($classReflect->implementsInterface($interface)) {
36+
return true;
37+
}
38+
39+
try {
40+
$interfaceReflect = new ReflectionClass($interface);
41+
} catch (ReflectionException $e) {
42+
//todo throw dedicated exception
43+
return false;
44+
}
45+
46+
if (!$interfaceReflect->isInterface()) {
47+
throw new InvalidArgumentException('Second argument must be an interface');
48+
}
49+
50+
foreach ($interfaceReflect->getMethods() as $interfaceMethod) {
51+
// check if class has same method name
52+
if (!$classReflect->hasMethod($interfaceMethod->getName())) {
53+
return false;
54+
}
55+
$classMethod = $classReflect->getMethod($interfaceMethod->getName());
56+
if (!reflection_methods_same($classMethod, $interfaceMethod)) {
57+
return false;
58+
}
59+
}
60+
61+
return true;
62+
}
63+
64+
/**
65+
* @param ReflectionMethod $method1
66+
* @param ReflectionMethod $method2
67+
* @return bool
68+
*/
69+
function reflection_methods_same(ReflectionMethod $method1, ReflectionMethod $method2): bool
70+
{
71+
$firstMethodModifiers = $method1->getModifiers();
72+
$secondMethodModifiers = $method1->getModifiers();
73+
if ($firstMethodModifiers != $secondMethodModifiers) {
74+
// interface methods are always abstract, so if it is the only difference, we ignore it
75+
if ($firstMethodModifiers ^ $secondMethodModifiers != ReflectionMethod::IS_ABSTRACT) {
76+
return false;
77+
}
78+
}
79+
if ($method1->getNumberOfParameters() != $method2->getNumberOfParameters()) {
80+
return false;
81+
}
82+
if ($method1->getNumberOfRequiredParameters() != $method2->getNumberOfRequiredParameters()) {
83+
return false;
84+
}
85+
$firstParams = $method1->getParameters();
86+
$secondParams = $method2->getParameters();
87+
/**
88+
* @var ReflectionParameter $interfaceParam
89+
* @var ReflectionParameter $classParam
90+
*/
91+
foreach ($secondParams as $i => $interfaceParam) {
92+
$classParam = $firstParams[$i];
93+
if (!reflection_params_same($interfaceParam, $classParam)) {
94+
return false;
95+
}
96+
}
97+
return true;
98+
}
99+
100+
/**
101+
* @param ReflectionParameter $param1
102+
* @param ReflectionParameter $param2
103+
* @return bool
104+
*/
105+
function reflection_params_same(ReflectionParameter $param1, ReflectionParameter $param2): bool
106+
{
107+
if ($param1->isPassedByReference() != $param2->isPassedByReference()) {
108+
return false;
109+
}
110+
if ($param1->isArray() != $param2->isArray()) {
111+
return false;
112+
}
113+
if ($param1->isCallable() != $param2->isCallable()) {
114+
return false;
115+
}
116+
if ($param1->isOptional() != $param2->isOptional()) {
117+
return false;
118+
}
119+
if ($param1->isVariadic() != $param2->isVariadic()) {
120+
return false;
121+
}
122+
$firstParamType = $param1->getType();
123+
$secondParamType = $param2->getType();
124+
125+
if (is_null($firstParamType) || is_null($secondParamType)) {
126+
return $firstParamType === $secondParamType;
127+
}
128+
129+
if (get_class($firstParamType) != get_class($secondParamType)) {
130+
return false;
131+
}
132+
if ($firstParamType instanceof ReflectionNamedType && $secondParamType instanceof ReflectionNamedType) {
133+
if ($firstParamType->getName() != $secondParamType->getName()) {
134+
return false;
135+
}
136+
}
137+
if ($firstParamType->isBuiltin() != $secondParamType->isBuiltin()) {
138+
return false;
139+
}
140+
if ($firstParamType->allowsNull() != $secondParamType->allowsNull()) {
141+
return false;
142+
}
143+
144+
return true;
145+
}

tests/FunctionsTest.php

+59
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace yaronius\ImplicitInterface\tests;
6+
7+
use PHPUnit\Framework\TestCase;
8+
9+
use function yaronius\ImplicitInterface\class_provides;
10+
11+
class FunctionsTest extends TestCase
12+
{
13+
public function testClassProvidesFunction()
14+
{
15+
$this->assertTrue(class_provides(AbstractFoo::class, FooInterface::class));
16+
$this->assertTrue(class_provides(Foo::class, FooInterface::class));
17+
18+
$this->assertTrue(class_provides(new Foo, FooInterface::class));
19+
20+
$this->assertTrue(class_provides(new Bar, BarInterface::class));
21+
$this->assertFalse(class_provides(new Bar, BazInterface::class));
22+
}
23+
}
24+
25+
abstract class AbstractFoo
26+
{
27+
abstract public function doDomething(string $param, int $param2): int;
28+
}
29+
30+
class Foo extends AbstractFoo
31+
{
32+
public function doDomething(string $param, int $param2): int
33+
{
34+
return 42;
35+
}
36+
}
37+
38+
interface FooInterface
39+
{
40+
function doDomething(string $param, int $param2): int;
41+
}
42+
43+
class Bar
44+
{
45+
function doDomethingElse(array $param, callable $param2, $untypedParam = null)
46+
{
47+
return 'bar';
48+
}
49+
}
50+
51+
interface BarInterface
52+
{
53+
function doDomethingElse(array $param, callable $param2, $untypedParam = null);
54+
}
55+
56+
interface BazInterface
57+
{
58+
function doDomethingElse(array $param, callable $param2, $untypedParam);
59+
}

tests/bootstrap.php

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
<?php
2+
3+
require_once __DIR__ . '/../vendor/autoload.php';

0 commit comments

Comments
 (0)