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
10 changes: 9 additions & 1 deletion documentation/mlc-cachedservicegenerator.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,6 @@ The `TestServiceInterface` will be put into the appropriate Interface directory.
Example Output of the `ddev mlc-make` command:
![mlc-make Command Output Example](images/mlc-make.png)


### Using the cached service

You basically have two options here.
Expand All @@ -99,6 +98,15 @@ If any service can't be updated (There are some reasons why this might happen) i
Example Output of the `ddev mlc-update` command:
![mlc-update Command Output Example](images/mlc-update.png)

## More about how the MLC generates the cached service

### Automatic Interface Transfer

The CSG will automatically transfer all interfaces implemented by the original service to the cached version. This means that if your original service implements `SomeInterface`, the cached version will also implement `SomeInterface` without you having to do anything extra.

#### Exceptions:
- Interfaces that define a constructor are not transferred as the cached version needs to have a different constructor signature. No way around this one.


## Advanced Usage

Expand Down
69 changes: 57 additions & 12 deletions src/CachedServiceGenerator/Service/MakeCachedServiceService.php
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,11 @@ public function __construct(
}

/**
* @return array<{interface: string, class: string}>
* @return array<{interface: string, class: string, issues: array<string>}>
*/
public function generateCachedService(string $class, ?string $cachedClass = null): array
{
$encounteredIssues = [];
$reflection = new ReflectionClass($class);

$classDotSeparated = str_replace('\\', '.', $class); # Used for CLI notation
Expand Down Expand Up @@ -95,12 +96,19 @@ public function generateCachedService(string $class, ?string $cachedClass = null
$useLines[] = "use {$mlcCacheableServiceAttribute->getAdditionalInterface()};";
$interfaces[] = $this->cleanupFqcnBasedOnUseLines($useLines, '\\' . $mlcCacheableServiceAttribute->getAdditionalInterface());
}
$reflectionInterfaces = $reflection->getInterfaceNames();
foreach ($reflectionInterfaces as $reflectionInterface) {
if (!in_array($reflectionInterface, $interfaces, true)) {
$useLines[] = "use {$reflectionInterface};";
$interfaces[] = $this->cleanupFqcnBasedOnUseLines($useLines, '\\' . $reflectionInterface);
}

$transferableInterfaces = $this->getTransferableInterfacesFromReflectionClass(
reflection: $reflection,
skipInterfaces: array_merge(
$interfaces, // skip all interfaces that are defined by other means
[$interfaceClass], // this ensures the interface itself will not be added to itself
),
issues: $encounteredIssues
);

foreach ($transferableInterfaces as $transferableInterface) {
$useLines[] = "use {$transferableInterface};";
$interfaces[] = $this->cleanupFqcnBasedOnUseLines($useLines, '\\' . $transferableInterface);
}

$useLines = array_unique($useLines);
Expand All @@ -124,7 +132,8 @@ class: $interfaceClass,
);

$useLines[] = "use {$interfaceClass};";
sort($useLines);
$interfaces = array_unique($interfaces);
sort($interfaces);

$classCode = RenderTemplateService::render('Class/CachedService', [
'ServiceNamespace' => $namespace,
Expand All @@ -150,6 +159,8 @@ class: $class,
interface: $interfaceClass,
);

$generatedFiles['issues'] = $encounteredIssues;

return $generatedFiles;
}

Expand All @@ -158,23 +169,23 @@ interface: $interfaceClass,
* Updates all cached services that are marked as autogenerated.
* @return array<array{service: string, status: string, message: string}>
*/
public function updateAllCachedServices(): array
public function updateAllCachedServices(?string $sourceDir = null): array
{
$updatedServices = [];
foreach (FetchAllCachedServices::getAllAutogeneratedServices() as $serviceData) {
foreach (FetchAllCachedServices::getAllAutogeneratedServices($sourceDir) as $serviceData) {
try {
$serviceName = $serviceData['serviceName'];

$getOriginalClass = $serviceData['cachedServiceAttr']->getOriginalServiceClass();
$this->generateCachedService(
$generationResult = $this->generateCachedService(
class: $getOriginalClass,
cachedClass: $serviceData['reflection']->getName(),
);

$updatedServices[] = [
'service' => $serviceName,
'status' => 'updated',
'message' => '',
'message' => $generationResult['issues'] ? implode(', ', $generationResult['issues']) : '',
];
} catch (MlcUpdateCachedServiceException $e) {
$updatedServices[] = [
Expand Down Expand Up @@ -392,6 +403,40 @@ private function generateDynamicMethodsCode(array $classMethods, array $useLines
];
}

/**
* @param string[] $skipInterfaces
* @return string[] List of interface names that should be implemented by the cached service, excluding the ones in $skipInterfaces
*/
private function getTransferableInterfacesFromReflectionClass(ReflectionClass $reflection, array $skipInterfaces, array &$issues = []): array
{
$interfaces = [];
foreach ($reflection->getInterfaceNames() as $interfaceName) {
if (
in_array($interfaceName, $interfaces, true)
|| in_array('\\' . $interfaceName, $interfaces, true)
|| in_array($interfaceName, $skipInterfaces, true)
|| in_array('\\' . $interfaceName, $skipInterfaces, true)
) {
continue;
}

$interfaceReflection = new ReflectionClass($interfaceName);
$interfaceMethods = $interfaceReflection->getMethods(ReflectionMethod::IS_PUBLIC);
foreach ($interfaceMethods as $method) {
if ($method->isConstructor()) {
$issues[] = "Skipped interface '{$interfaceName}' because it contains a constructor.";

continue 2;
}
}


$interfaces[] = $interfaceName;
}

return $interfaces;
}

/**
* @return array<ReflectionMethod>
*/
Expand Down
7 changes: 7 additions & 0 deletions src/Commands/make.php
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,13 @@ function resolveServiceClass(string $service): ?string
['Interface', $targetFile['interface']],
],
);

if (!empty($targetFile['issues'])) {
PrintTools::error("However, some issues were encountered during generation:");
foreach ($targetFile['issues'] as $issue) {
PrintTools::line("- " . $issue);
}
}
echo PHP_EOL . PHP_EOL;

} catch (Exception $e) {
Expand Down
17 changes: 16 additions & 1 deletion src/Commands/update.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,30 @@

require_once __DIR__ . '/AutoloadShenanigans.php';

use Tbessenreither\MultiLevelCache\CachedServiceGenerator\Service\ArgParser;
use Tbessenreither\MultiLevelCache\CachedServiceGenerator\Service\MakeCachedServiceService;
use Tbessenreither\MultiLevelCache\CachedServiceGenerator\Service\PrintTools;

PrintTools::headline('Updating all cached services...');

PrintTools::info('Parsing command line arguments...');
$arguments = ArgParser::parse();
PrintTools::table(
['Argument', 'Value'],
array_map(
fn ($key, $value) => [$key, $value],
array_keys($arguments),
$arguments,
),
);
if (!isset($arguments['source-dir'])) {
$arguments['source-dir'] = null;
}
echo PHP_EOL . PHP_EOL;

PrintTools::subHeadline('Starting update process for cached services...');
$service = new MakeCachedServiceService();
$result = $service->updateAllCachedServices();
$result = $service->updateAllCachedServices($arguments['source-dir']);

PrintTools::table(
['Service', 'Status', 'Message'],
Expand Down
39 changes: 39 additions & 0 deletions tests/Commands/MakeTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ class MakeTest extends TestCase
private string $interfaceDirectory = __DIR__ . '/TestFiles/Interface/Service';

private array $keepInterfaces = [
'ConstructorInterface.php',
'InterfaceA.php',
'InterfaceB.php',
'InterfaceC.php',
Expand Down Expand Up @@ -115,6 +116,44 @@ public function testMakeWithInterfaces(): void

}

public function testMakeWithConstructorInterfaces(): void
{
$makePath = realpath(__DIR__ . '/../../src/Commands/make.php');
$this->assertNotFalse($makePath);

$classDotNotation = str_replace('\\', '.', 'Tbessenreither\MultiLevelCache\Tests\Commands\TestFiles\Service\ServiceWithConstructorInterface');
$cachedServiceFile = $this->serviceDirectory . '/ServiceWithConstructorInterfaceCached.php';
$interfaceFile = $this->interfaceDirectory . '/ServiceWithConstructorInterfaceInterface.php';

$command = sprintf('php %s %s 2>&1', $makePath, '--service=' . $classDotNotation);
exec($command, $output, $resultCode);
$this->assertSame(0, $resultCode, implode("\n", $output));

$this->checkFileSyntax($cachedServiceFile);
$this->checkFileSyntax($interfaceFile);

$outputString = implode(PHP_EOL, $output);

$this->assertStringContainsString('Cached service generated successfully.', $outputString);
$this->assertStringContainsString($cachedServiceFile, $outputString);
$this->assertStringContainsString($interfaceFile, $outputString);

$cachedFileContent = file_get_contents($cachedServiceFile);
$this->assertNotFalse($cachedFileContent);

$lines = explode(PHP_EOL, $cachedFileContent);
$lineWithInterfaces = null;
foreach ($lines as $line) {
if (str_contains($line, 'class ServiceWithConstructorInterfaceCached implements')) {
$lineWithInterfaces = $line;

break;
}
}
$this->assertNotNull($lineWithInterfaces, 'Line with interfaces not found.');
$this->assertStringNotContainsString(' ConstructorInterface', $lineWithInterfaces, 'ConstructorInterface should not be implemented by the cached class.');
}

private function checkFileSyntax(string $file): void
{
if (!file_exists($file)) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<?php

declare(strict_types=1);

namespace Tbessenreither\MultiLevelCache\Tests\Commands\TestFiles\Interface\Service;

interface ConstructorInterface
{
public function __construct(string $string);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<?php

declare(strict_types=1);

namespace Tbessenreither\MultiLevelCache\Tests\Commands\TestFiles\Service;

use Tbessenreither\MultiLevelCache\Tests\Commands\TestFiles\Interface\Service\ConstructorInterface;

class ServiceWithConstructorInterface implements ConstructorInterface
{
public function __construct(private string $string)
{

}
public function methodA(): string
{
return 'methodA';
}

public function methodB(): string
{
return 'methodB';
}
}
Loading