Skip to content

Commit 5a7b69d

Browse files
committed
feat: add PhpScriptApplication as a _HANDLER-based fallback
1 parent d7571ba commit 5a7b69d

6 files changed

Lines changed: 335 additions & 11 deletions

File tree

src/Application/ApplicationFactory.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,9 @@ class ApplicationFactory
3434

3535
// Laravel
3636
LaravelApplication::class,
37+
38+
// PHP script fallback via _HANDLER
39+
PhpScriptApplication::class,
3740
];
3841

3942
/**
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
/*
6+
* This file is part of Ymir PHP Runtime.
7+
*
8+
* (c) Carl Alexander <support@ymirapp.com>
9+
*
10+
* For the full copyright and license information, please view the LICENSE
11+
* file that was distributed with this source code.
12+
*/
13+
14+
namespace Ymir\Runtime\Application;
15+
16+
use Ymir\Runtime\Lambda\Handler\Http\PhpScriptHttpEventHandler;
17+
use Ymir\Runtime\Lambda\Handler\LambdaEventHandlerCollection;
18+
19+
/**
20+
* PHP script runtime application.
21+
*/
22+
class PhpScriptApplication extends AbstractApplication
23+
{
24+
/**
25+
* {@inheritDoc}
26+
*/
27+
public static function present(string $directory): bool
28+
{
29+
return true;
30+
}
31+
32+
/**
33+
* {@inheritDoc}
34+
*/
35+
public function getWebsiteHandlers(): LambdaEventHandlerCollection
36+
{
37+
return $this->getEventHandlerCollection([
38+
new PhpScriptHttpEventHandler($this->context->getLogger(), $this->context->getPhpFpmProcess(), $this->context->getRootDirectory()),
39+
]);
40+
}
41+
}
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
/*
6+
* This file is part of Ymir PHP Runtime.
7+
*
8+
* (c) Carl Alexander <support@ymirapp.com>
9+
*
10+
* For the full copyright and license information, please view the LICENSE
11+
* file that was distributed with this source code.
12+
*/
13+
14+
namespace Ymir\Runtime\Lambda\Handler\Http;
15+
16+
use Ymir\Runtime\FastCgi\PhpFpmProcess;
17+
use Ymir\Runtime\Lambda\InvocationEvent\HttpRequestEvent;
18+
use Ymir\Runtime\Lambda\InvocationEvent\InvocationEventInterface;
19+
use Ymir\Runtime\Logger;
20+
21+
/**
22+
* Lambda invocation event handler for a specific PHP script.
23+
*/
24+
class PhpScriptHttpEventHandler extends AbstractPhpFpmRequestEventHandler
25+
{
26+
/**
27+
* The path to the PHP script that this event handler uses.
28+
*
29+
* @var string
30+
*/
31+
private $scriptFilePath;
32+
33+
/**
34+
* Constructor.
35+
*/
36+
public function __construct(Logger $logger, PhpFpmProcess $process, string $rootDirectory, ?string $scriptFilename = null)
37+
{
38+
parent::__construct($logger, $process, $rootDirectory);
39+
40+
if (!is_string($scriptFilename)) {
41+
$scriptFilename = getenv('_HANDLER') ?: 'index.php';
42+
}
43+
44+
$this->scriptFilePath = $this->rootDirectory.'/'.ltrim($scriptFilename, '/');
45+
}
46+
47+
/**
48+
* {@inheritdoc}
49+
*/
50+
public function canHandle(InvocationEventInterface $event): bool
51+
{
52+
return parent::canHandle($event)
53+
&& false !== stripos($this->scriptFilePath, '.php')
54+
&& file_exists($this->scriptFilePath);
55+
}
56+
57+
/**
58+
* {@inheritdoc}
59+
*/
60+
protected function getScriptFilePath(HttpRequestEvent $event): string
61+
{
62+
return $this->scriptFilePath;
63+
}
64+
}

tests/Unit/Application/ApplicationFactoryTest.php

Lines changed: 8 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,9 @@
1717
use Ymir\Runtime\Application\ApplicationFactory;
1818
use Ymir\Runtime\Application\BedrockApplication;
1919
use Ymir\Runtime\Application\LaravelApplication;
20+
use Ymir\Runtime\Application\PhpScriptApplication;
2021
use Ymir\Runtime\Application\RadicleApplication;
2122
use Ymir\Runtime\Application\WordPressApplication;
22-
use Ymir\Runtime\Exception\ApplicationInitializationException;
2323
use Ymir\Runtime\RuntimeContext;
2424
use Ymir\Runtime\Tests\Mock\FunctionMockTrait;
2525
use Ymir\Runtime\Tests\Mock\LambdaRuntimeApiClientMockTrait;
@@ -65,6 +65,13 @@ public function testCreateFromContextReturnsLaravelApplication(): void
6565
$this->assertInstanceOf(LaravelApplication::class, ApplicationFactory::createFromContext($context));
6666
}
6767

68+
public function testCreateFromContextReturnsPhpScriptApplicationAsHandlerFallback(): void
69+
{
70+
$context = new RuntimeContext($this->getLoggerMock(), $this->getLambdaRuntimeApiClientMock(), 'us-east-1', $this->tempDir);
71+
72+
$this->assertInstanceOf(PhpScriptApplication::class, ApplicationFactory::createFromContext($context));
73+
}
74+
6875
public function testCreateFromContextReturnsRadicleApplication(): void
6976
{
7077
mkdir($this->tempDir.'/public/content/mu-plugins', 0777, true);
@@ -85,16 +92,6 @@ public function testCreateFromContextReturnsWordPressApplication(): void
8592
$this->assertInstanceOf(WordPressApplication::class, ApplicationFactory::createFromContext($context));
8693
}
8794

88-
public function testCreateFromContextThrowsExceptionWhenNoApplicationFound(): void
89-
{
90-
$this->expectException(ApplicationInitializationException::class);
91-
$this->expectExceptionMessage('Unable to create runtime application');
92-
93-
$context = new RuntimeContext($this->getLoggerMock(), $this->getLambdaRuntimeApiClientMock(), 'us-east-1', $this->tempDir);
94-
95-
ApplicationFactory::createFromContext($context);
96-
}
97-
9895
private function removeDirectory(string $dir): void
9996
{
10097
if (!is_dir($dir)) {
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
/*
6+
* This file is part of Ymir PHP Runtime.
7+
*
8+
* (c) Carl Alexander <support@ymirapp.com>
9+
*
10+
* For the full copyright and license information, please view the LICENSE
11+
* file that was distributed with this source code.
12+
*/
13+
14+
namespace Ymir\Runtime\Tests\Unit\Application;
15+
16+
use PHPUnit\Framework\TestCase;
17+
use Ymir\Runtime\Application\PhpScriptApplication;
18+
use Ymir\Runtime\Lambda\Handler\Http\PhpScriptHttpEventHandler;
19+
use Ymir\Runtime\Lambda\Handler\PingLambdaEventHandler;
20+
use Ymir\Runtime\Lambda\Handler\WarmUpEventHandler;
21+
use Ymir\Runtime\RuntimeContext;
22+
use Ymir\Runtime\Tests\Mock\FunctionMockTrait;
23+
use Ymir\Runtime\Tests\Mock\LambdaRuntimeApiClientMockTrait;
24+
use Ymir\Runtime\Tests\Mock\LoggerMockTrait;
25+
use Ymir\Runtime\Tests\Mock\PhpFpmProcessMockTrait;
26+
27+
class PhpScriptApplicationTest extends TestCase
28+
{
29+
use FunctionMockTrait;
30+
use LambdaRuntimeApiClientMockTrait;
31+
use LoggerMockTrait;
32+
use PhpFpmProcessMockTrait;
33+
34+
private $tempDir;
35+
36+
protected function setUp(): void
37+
{
38+
$this->tempDir = sys_get_temp_dir().'/ymir_test_'.uniqid();
39+
mkdir($this->tempDir, 0777, true);
40+
}
41+
42+
protected function tearDown(): void
43+
{
44+
$this->removeDirectory($this->tempDir);
45+
}
46+
47+
public function testGetWebsiteHandlers(): void
48+
{
49+
$logger = $this->getLoggerMock();
50+
$process = $this->getPhpFpmProcessMock();
51+
$context = new RuntimeContext($logger, $this->getLambdaRuntimeApiClientMock(), 'us-east-1', $this->tempDir, null, $process);
52+
53+
$application = new PhpScriptApplication($context);
54+
$handlers = $application->getWebsiteHandlers();
55+
56+
$property = new \ReflectionProperty($handlers, 'handlers');
57+
$property->setAccessible(true);
58+
$handlers = $property->getValue($handlers);
59+
60+
$this->assertCount(3, $handlers);
61+
$this->assertInstanceOf(PingLambdaEventHandler::class, $handlers[0]);
62+
$this->assertInstanceOf(WarmUpEventHandler::class, $handlers[1]);
63+
$this->assertInstanceOf(PhpScriptHttpEventHandler::class, $handlers[2]);
64+
}
65+
66+
public function testPresentAlwaysReturnsTrueForHandlerFallback(): void
67+
{
68+
$this->assertTrue(PhpScriptApplication::present($this->tempDir));
69+
}
70+
71+
private function removeDirectory(string $dir): void
72+
{
73+
if (!is_dir($dir)) {
74+
return;
75+
}
76+
77+
$files = array_diff(scandir($dir), ['.', '..']);
78+
foreach ($files as $file) {
79+
(is_dir("$dir/$file")) ? $this->removeDirectory("$dir/$file") : unlink("$dir/$file");
80+
}
81+
82+
rmdir($dir);
83+
}
84+
}
Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
/*
6+
* This file is part of Ymir PHP Runtime.
7+
*
8+
* (c) Carl Alexander <support@ymirapp.com>
9+
*
10+
* For the full copyright and license information, please view the LICENSE
11+
* file that was distributed with this source code.
12+
*/
13+
14+
namespace Ymir\Runtime\Tests\Unit\Lambda\Handler\Http;
15+
16+
use PHPUnit\Framework\TestCase;
17+
use Ymir\Runtime\FastCgi\FastCgiRequest;
18+
use Ymir\Runtime\Lambda\Handler\Http\PhpScriptHttpEventHandler;
19+
use Ymir\Runtime\Lambda\Response\Http\FastCgiHttpResponse;
20+
use Ymir\Runtime\Tests\Mock\FunctionMockTrait;
21+
use Ymir\Runtime\Tests\Mock\HttpRequestEventMockTrait;
22+
use Ymir\Runtime\Tests\Mock\InvocationEventInterfaceMockTrait;
23+
use Ymir\Runtime\Tests\Mock\LoggerMockTrait;
24+
use Ymir\Runtime\Tests\Mock\PhpFpmProcessMockTrait;
25+
26+
class PhpScriptHttpEventHandlerTest extends TestCase
27+
{
28+
use FunctionMockTrait;
29+
use HttpRequestEventMockTrait;
30+
use InvocationEventInterfaceMockTrait;
31+
use LoggerMockTrait;
32+
use PhpFpmProcessMockTrait;
33+
34+
public function testCanHandleNonExistentScriptFile(): void
35+
{
36+
$file_exists = $this->getFunctionMock($this->getNamespace(PhpScriptHttpEventHandler::class), 'file_exists');
37+
$process = $this->getPhpFpmProcessMock();
38+
39+
$file_exists->expects($this->any())
40+
->willReturn(false);
41+
42+
$handler = new PhpScriptHttpEventHandler($this->getLoggerMock(), $process, '/', 'tmp.php');
43+
44+
$this->assertFalse($handler->canHandle($this->getHttpRequestEventMock()));
45+
}
46+
47+
public function testCanHandleWithValidScriptFile(): void
48+
{
49+
$file_exists = $this->getFunctionMock($this->getNamespace(PhpScriptHttpEventHandler::class), 'file_exists');
50+
$process = $this->getPhpFpmProcessMock();
51+
52+
$file_exists->expects($this->once())
53+
->with($this->identicalTo('/tmp.php'))
54+
->willReturn(true);
55+
56+
$handler = new PhpScriptHttpEventHandler($this->getLoggerMock(), $process, '', 'tmp.php');
57+
58+
$this->assertTrue($handler->canHandle($this->getHttpRequestEventMock()));
59+
}
60+
61+
public function testCanHandleWrongEventType(): void
62+
{
63+
$file_exists = $this->getFunctionMock($this->getNamespace(PhpScriptHttpEventHandler::class), 'file_exists');
64+
$process = $this->getPhpFpmProcessMock();
65+
66+
$file_exists->expects($this->any())
67+
->with($this->identicalTo('/tmp.php'))
68+
->willReturn(true);
69+
70+
$handler = new PhpScriptHttpEventHandler($this->getLoggerMock(), $process, '/', 'tmp.php');
71+
72+
$this->assertFalse($handler->canHandle($this->getInvocationEventInterfaceMock()));
73+
}
74+
75+
public function testCanHandleWrongScriptFileType(): void
76+
{
77+
$process = $this->getPhpFpmProcessMock();
78+
79+
$handler = new PhpScriptHttpEventHandler($this->getLoggerMock(), $process, '/', 'tmp');
80+
81+
$this->assertFalse($handler->canHandle($this->getHttpRequestEventMock()));
82+
}
83+
84+
public function testHandleCreatesFastCgiHttpResponseWithPayloadVersion1(): void
85+
{
86+
$event = $this->getHttpRequestEventMock();
87+
$file_exists = $this->getFunctionMock($this->getNamespace(PhpScriptHttpEventHandler::class), 'file_exists');
88+
$process = $this->getPhpFpmProcessMock();
89+
90+
$event->expects($this->exactly(2))
91+
->method('getPath')
92+
->willReturn('tmp');
93+
94+
$event->expects($this->once())
95+
->method('getPayloadVersion')
96+
->willReturn('1.0');
97+
98+
$process->expects($this->once())
99+
->method('handle')
100+
->with($this->isInstanceOf(FastCgiRequest::class));
101+
102+
$file_exists->expects($this->any())
103+
->willReturn(true);
104+
105+
$handler = new PhpScriptHttpEventHandler($this->getLoggerMock(), $process, '/', 'tmp.php');
106+
107+
$this->assertInstanceOf(FastCgiHttpResponse::class, $handler->handle($event));
108+
}
109+
110+
public function testHandleCreatesFastCgiHttpResponseWithPayloadVersion2(): void
111+
{
112+
$event = $this->getHttpRequestEventMock();
113+
$file_exists = $this->getFunctionMock($this->getNamespace(PhpScriptHttpEventHandler::class), 'file_exists');
114+
$process = $this->getPhpFpmProcessMock();
115+
116+
$event->expects($this->exactly(2))
117+
->method('getPath')
118+
->willReturn('tmp');
119+
120+
$event->expects($this->once())
121+
->method('getPayloadVersion')
122+
->willReturn('2.0');
123+
124+
$process->expects($this->once())
125+
->method('handle')
126+
->with($this->isInstanceOf(FastCgiRequest::class));
127+
128+
$file_exists->expects($this->any())
129+
->willReturn(true);
130+
131+
$handler = new PhpScriptHttpEventHandler($this->getLoggerMock(), $process, '/', 'tmp.php');
132+
133+
$this->assertInstanceOf(FastCgiHttpResponse::class, $handler->handle($event));
134+
}
135+
}

0 commit comments

Comments
 (0)