Skip to content

Commit 592568c

Browse files
authored
Performance optimizations and caching (#698)
* Combine addTypeNamespace() and addControllerNamespace() into addNamespace() * Refactor CachedDocBlockFactory to use existing caching infrastructure * Refactor field name * Refactor code to use ClassFinder instead of separating namespaces * Make caches only recompute on file changes * Refactor EnumTypeMapper to use cached doc block factory * Fix FileModificationClassFinderBoundCache * Use Kcs ClassFinder cache * One more tiny optimization for unchanged cache * Change name of ClassFinderBoundCache * Code style * PHPStan * Some fixes and renames * Fix broken class bound cache after merge * Fix some tests and PHPStan * Fix all failing tests * Fix one failing test on CI * Fix one failing test on CI, again * Fix --prefer-lowest tests * Some simplifications and tests * Simplify cached doc blocks * Simplify cached doc blocks * More tests for doc block factories * Tests for the Discovery namespace * Fix the docs build on CI and broken links * Add a changelog entry * Deprecate setGlobTTL() instead of removing it * Code style
1 parent 069d62a commit 592568c

File tree

72 files changed

+1700
-1415
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

72 files changed

+1700
-1415
lines changed

.github/workflows/doc_generation.yml

+1-1
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ jobs:
2424
- name: "Setup NodeJS"
2525
uses: actions/setup-node@v4
2626
with:
27-
node-version: '16.x'
27+
node-version: '20.x'
2828

2929
- name: "Yarn install"
3030
run: yarn install

composer.json

+2-2
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@
2424
"symfony/cache": "^4.3 || ^5 || ^6 || ^7",
2525
"symfony/expression-language": "^4 || ^5 || ^6 || ^7",
2626
"webonyx/graphql-php": "^v15.0",
27-
"kcs/class-finder": "^0.5.0"
27+
"kcs/class-finder": "^0.5.1"
2828
},
2929
"require-dev": {
3030
"beberlei/porpaginas": "^1.2 || ^2.0",
@@ -34,7 +34,7 @@
3434
"myclabs/php-enum": "^1.6.6",
3535
"php-coveralls/php-coveralls": "^2.1",
3636
"phpstan/extension-installer": "^1.1",
37-
"phpstan/phpstan": "^1.9",
37+
"phpstan/phpstan": "^1.11",
3838
"phpunit/phpunit": "^10.1 || ^11.0",
3939
"symfony/var-dumper": "^5.4 || ^6.0 || ^7",
4040
"thecodingmachine/phpstan-strict-rules": "^1.0"

examples/no-framework/index.php

+1-2
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,7 @@
2424
]);
2525

2626
$factory = new SchemaFactory($cache, $container);
27-
$factory->addControllerNamespace('App\\Controllers')
28-
->addTypeNamespace('App');
27+
$factory->addNamespace('App');
2928

3029
$schema = $factory->createSchema();
3130

src/AggregateControllerQueryProvider.php

+21-14
Original file line numberDiff line numberDiff line change
@@ -8,16 +8,12 @@
88
use Psr\Container\ContainerInterface;
99
use TheCodingMachine\GraphQLite\Mappers\DuplicateMappingException;
1010

11-
use function array_filter;
12-
use function array_intersect_key;
13-
use function array_keys;
1411
use function array_map;
1512
use function array_merge;
1613
use function array_sum;
1714
use function array_values;
1815
use function assert;
1916
use function count;
20-
use function reset;
2117
use function sort;
2218

2319
/**
@@ -94,18 +90,29 @@ private function flattenList(array $list): array
9490
}
9591

9692
// We have an issue, let's detect the duplicate
97-
$duplicates = array_intersect_key(...array_values($list));
98-
// Let's display an error from the first one.
99-
$firstDuplicate = reset($duplicates);
100-
assert($firstDuplicate instanceof FieldDefinition);
93+
$queriesByName = [];
94+
$duplicateClasses = null;
95+
$duplicateQueryName = null;
10196

102-
$duplicateName = $firstDuplicate->name;
97+
foreach ($list as $class => $queries) {
98+
foreach ($queries as $query => $field) {
99+
$duplicatedClass = $queriesByName[$query] ?? null;
103100

104-
$classes = array_keys(array_filter($list, static function (array $fields) use ($duplicateName) {
105-
return isset($fields[$duplicateName]);
106-
}));
107-
sort($classes);
101+
if (! $duplicatedClass) {
102+
$queriesByName[$query] = $class;
108103

109-
throw DuplicateMappingException::createForQueryInTwoControllers($classes[0], $classes[1], $duplicateName);
104+
continue;
105+
}
106+
107+
$duplicateClasses = [$duplicatedClass, $class];
108+
$duplicateQueryName = $query;
109+
}
110+
}
111+
112+
assert($duplicateClasses !== null && $duplicateQueryName !== null);
113+
114+
sort($duplicateClasses);
115+
116+
throw DuplicateMappingException::createForQueryInTwoControllers($duplicateClasses[0], $duplicateClasses[1], $duplicateQueryName);
110117
}
111118
}

src/Cache/ClassBoundCache.php

+24
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace TheCodingMachine\GraphQLite\Cache;
6+
7+
use ReflectionClass;
8+
9+
interface ClassBoundCache
10+
{
11+
/**
12+
* @param callable(): TReturn $resolver
13+
*
14+
* @return TReturn
15+
*
16+
* @template TReturn
17+
*/
18+
public function get(
19+
ReflectionClass $reflectionClass,
20+
callable $resolver,
21+
string $key,
22+
bool $withInheritance = false,
23+
): mixed;
24+
}

src/Cache/ClassBoundCacheContract.php

-43
This file was deleted.

src/Cache/ClassBoundCacheContractFactory.php

-15
This file was deleted.

src/Cache/ClassBoundCacheContractFactoryInterface.php

-12
This file was deleted.

src/Cache/ClassBoundCacheContractInterface.php

-13
This file was deleted.

src/Cache/FilesSnapshot.php

+86
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace TheCodingMachine\GraphQLite\Cache;
6+
7+
use ReflectionClass;
8+
9+
use function array_unique;
10+
use function Safe\filemtime;
11+
12+
class FilesSnapshot
13+
{
14+
/** @param array<string, int> $dependencies */
15+
private function __construct(
16+
private readonly array $dependencies,
17+
)
18+
{
19+
}
20+
21+
/** @param list<string> $files */
22+
public static function for(array $files): self
23+
{
24+
$dependencies = [];
25+
26+
foreach (array_unique($files) as $file) {
27+
$dependencies[$file] = filemtime($file);
28+
}
29+
30+
return new self($dependencies);
31+
}
32+
33+
public static function forClass(ReflectionClass $class, bool $withInheritance = false): self
34+
{
35+
return self::for(
36+
self::dependencies($class, $withInheritance),
37+
);
38+
}
39+
40+
public static function alwaysUnchanged(): self
41+
{
42+
return new self([]);
43+
}
44+
45+
/** @return list<string> */
46+
private static function dependencies(ReflectionClass $class, bool $withInheritance = false): array
47+
{
48+
$filename = $class->getFileName();
49+
50+
// Internal classes are treated as always the same, e.g. you'll have to drop the cache between PHP versions.
51+
if ($filename === false) {
52+
return [];
53+
}
54+
55+
$files = [$filename];
56+
57+
if (! $withInheritance) {
58+
return $files;
59+
}
60+
61+
if ($class->getParentClass() !== false) {
62+
$files = [...$files, ...self::dependencies($class->getParentClass(), $withInheritance)];
63+
}
64+
65+
foreach ($class->getTraits() as $trait) {
66+
$files = [...$files, ...self::dependencies($trait, $withInheritance)];
67+
}
68+
69+
foreach ($class->getInterfaces() as $interface) {
70+
$files = [...$files, ...self::dependencies($interface, $withInheritance)];
71+
}
72+
73+
return $files;
74+
}
75+
76+
public function changed(): bool
77+
{
78+
foreach ($this->dependencies as $filename => $modificationTime) {
79+
if ($modificationTime !== filemtime($filename)) {
80+
return true;
81+
}
82+
}
83+
84+
return false;
85+
}
86+
}

src/Cache/SnapshotClassBoundCache.php

+41
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace TheCodingMachine\GraphQLite\Cache;
6+
7+
use Psr\SimpleCache\CacheInterface;
8+
use ReflectionClass;
9+
10+
use function str_replace;
11+
12+
class SnapshotClassBoundCache implements ClassBoundCache
13+
{
14+
/** @param callable(ReflectionClass, bool $withInheritance): FilesSnapshot $filesSnapshotFactory */
15+
public function __construct(
16+
private readonly CacheInterface $cache,
17+
private readonly mixed $filesSnapshotFactory,
18+
) {
19+
}
20+
21+
public function get(ReflectionClass $reflectionClass, callable $resolver, string $key = '', bool $withInheritance = false): mixed
22+
{
23+
$cacheKey = $reflectionClass->getName() . '__' . $key;
24+
$cacheKey = str_replace(['\\', '{', '}', '(', ')', '/', '@', ':'], '_', $cacheKey);
25+
26+
$item = $this->cache->get($cacheKey);
27+
28+
if ($item !== null && ! $item['snapshot']->changed()) {
29+
return $item['data'];
30+
}
31+
32+
$item = [
33+
'data' => $resolver(),
34+
'snapshot' => ($this->filesSnapshotFactory)($reflectionClass, $withInheritance),
35+
];
36+
37+
$this->cache->set($cacheKey, $item);
38+
39+
return $item['data'];
40+
}
41+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace TheCodingMachine\GraphQLite\Discovery\Cache;
6+
7+
use ReflectionClass;
8+
use TheCodingMachine\GraphQLite\Discovery\ClassFinder;
9+
10+
/**
11+
* Cache that computes a final value based on class that exist in the application found with
12+
* the {@see ClassFinder}, and one that allows invalidating only parts of the cache when those
13+
* classes change, instead of having to invalidate the whole cache on every change.
14+
*/
15+
interface ClassFinderComputedCache
16+
{
17+
/**
18+
* Compute the value of the cache. The $finder and $key are self-explanatory; the $map and $reduce need
19+
* a bit of an explanation: $map is called with each reflection found by $finder, and expects any value to be returned.
20+
* It will then be stored in a Map<string (filename), TEntry (return from $map)>. Once all classes are iterated,
21+
* $reduce will then be called with that map, and it's final result is returned.
22+
*
23+
* Now the point of this is now whenever file A changes, we can automatically remove entries generated for it
24+
* and simply call $map only for classes from file A, leaving all other entries untouched and not having to
25+
* waste resources on the rest of them. We then only need to call the cheap $reduce and have the final result :)
26+
*
27+
* @param callable(ReflectionClass<object>): TEntry $map
28+
* @param callable(array<string, TEntry>): TReturn $reduce
29+
*
30+
* @return TReturn
31+
*
32+
* @template TEntry of mixed
33+
* @template TReturn of mixed
34+
*/
35+
public function compute(
36+
ClassFinder $classFinder,
37+
string $key,
38+
callable $map,
39+
callable $reduce,
40+
): mixed;
41+
}

0 commit comments

Comments
 (0)