Handle Client responses
parent
bec24383d4
commit
9283ba2148
|
@ -3,9 +3,9 @@ declare(strict_types = 1);
|
|||
|
||||
namespace LanguageServer\Client;
|
||||
|
||||
use AdvancedJsonRpc\Notification as NotificationBody;
|
||||
use LanguageServer\ProtocolWriter;
|
||||
use LanguageServer\ClientHandler;
|
||||
use LanguageServer\Protocol\Message;
|
||||
use Sabre\Event\Promise;
|
||||
|
||||
/**
|
||||
* Provides method handlers for all textDocument/* methods
|
||||
|
@ -13,13 +13,13 @@ use LanguageServer\Protocol\Message;
|
|||
class TextDocument
|
||||
{
|
||||
/**
|
||||
* @var ProtocolWriter
|
||||
* @var ClientHandler
|
||||
*/
|
||||
private $protocolWriter;
|
||||
private $handler;
|
||||
|
||||
public function __construct(ProtocolWriter $protocolWriter)
|
||||
public function __construct(ClientHandler $handler)
|
||||
{
|
||||
$this->protocolWriter = $protocolWriter;
|
||||
$this->handler = $handler;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -27,15 +27,13 @@ class TextDocument
|
|||
*
|
||||
* @param string $uri
|
||||
* @param Diagnostic[] $diagnostics
|
||||
* @return Promise <void>
|
||||
*/
|
||||
public function publishDiagnostics(string $uri, array $diagnostics)
|
||||
public function publishDiagnostics(string $uri, array $diagnostics): Promise
|
||||
{
|
||||
$this->protocolWriter->write(new Message(new NotificationBody(
|
||||
'textDocument/publishDiagnostics',
|
||||
(object)[
|
||||
return $this->handler->notify('textDocument/publishDiagnostics', [
|
||||
'uri' => $uri,
|
||||
'diagnostics' => $diagnostics
|
||||
]
|
||||
)));
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,9 +3,9 @@ declare(strict_types = 1);
|
|||
|
||||
namespace LanguageServer\Client;
|
||||
|
||||
use AdvancedJsonRpc\Notification as NotificationBody;
|
||||
use LanguageServer\ProtocolWriter;
|
||||
use LanguageServer\ClientHandler;
|
||||
use LanguageServer\Protocol\Message;
|
||||
use Sabre\Event\Promise;
|
||||
|
||||
/**
|
||||
* Provides method handlers for all window/* methods
|
||||
|
@ -13,30 +13,26 @@ use LanguageServer\Protocol\Message;
|
|||
class Window
|
||||
{
|
||||
/**
|
||||
* @var ProtocolWriter
|
||||
* @var ClientHandler
|
||||
*/
|
||||
private $protocolWriter;
|
||||
private $handler;
|
||||
|
||||
public function __construct(ProtocolWriter $protocolWriter)
|
||||
public function __construct(ClientHandler $handler)
|
||||
{
|
||||
$this->protocolWriter = $protocolWriter;
|
||||
$this->handler = $handler;
|
||||
}
|
||||
|
||||
/**
|
||||
* The show message notification is sent from a server to a client to ask the client to display a particular message in the user interface.
|
||||
* The show message notification is sent from a server to a client
|
||||
* to ask the client to display a particular message in the user interface.
|
||||
*
|
||||
* @param int $type
|
||||
* @param string $message
|
||||
* @return Promise <void>
|
||||
*/
|
||||
public function showMessage(int $type, string $message)
|
||||
public function showMessage(int $type, string $message): Promise
|
||||
{
|
||||
$this->protocolWriter->write(new Message(new NotificationBody(
|
||||
'window/showMessage',
|
||||
(object)[
|
||||
'type' => $type,
|
||||
'message' => $message
|
||||
]
|
||||
)));
|
||||
return $this->handler->notify('window/showMessage', ['type' => $type, 'message' => $message]);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -44,15 +40,10 @@ class Window
|
|||
*
|
||||
* @param int $type
|
||||
* @param string $message
|
||||
* @return Promise <void>
|
||||
*/
|
||||
public function logMessage(int $type, string $message)
|
||||
public function logMessage(int $type, string $message): Promise
|
||||
{
|
||||
$this->protocolWriter->write(new Message(new NotificationBody(
|
||||
'window/logMessage',
|
||||
(object)[
|
||||
'type' => $type,
|
||||
'message' => $message
|
||||
]
|
||||
)));
|
||||
return $this->handler->notify('window/logMessage', ['type' => $type, 'message' => $message]);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,81 @@
|
|||
<?php
|
||||
declare(strict_types = 1);
|
||||
|
||||
namespace LanguageServer;
|
||||
|
||||
use AdvancedJsonRpc;
|
||||
use Sabre\Event\Promise;
|
||||
|
||||
class ClientHandler
|
||||
{
|
||||
/**
|
||||
* @var ProtocolReader
|
||||
*/
|
||||
private $protocolReader;
|
||||
|
||||
/**
|
||||
* @var ProtocolWriter
|
||||
*/
|
||||
private $protocolWriter;
|
||||
|
||||
/**
|
||||
* @var IdGenerator
|
||||
*/
|
||||
private $idGenerator;
|
||||
|
||||
public function __construct(ProtocolReader $protocolReader, ProtocolWriter $protocolWriter)
|
||||
{
|
||||
$this->protocolReader = $protocolReader;
|
||||
$this->protocolWriter = $protocolWriter;
|
||||
$this->idGenerator = new IdGenerator;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a request to the client and returns a promise that is resolved with the result or rejected with the error
|
||||
*
|
||||
* @param string $method The method to call
|
||||
* @param array|object $params The method parameters
|
||||
* @return Promise <mixed> Resolved with the result of the request or rejected with an error
|
||||
*/
|
||||
public function request(string $method, $params): Promise
|
||||
{
|
||||
$id = $this->idGenerator->generate();
|
||||
return $this->protocolWriter->write(
|
||||
new Protocol\Message(
|
||||
new AdvancedJsonRpc\Request($id, $method, (object)$params)
|
||||
)
|
||||
)->then(function () use ($id) {
|
||||
return new Promise(function ($resolve, $reject) use ($id) {
|
||||
$listener = function (Protocol\Message $msg) use ($id, $listener) {
|
||||
if (AdvancedJsonRpc\Response::isResponse($msg->body) && $msg->body->id === $id) {
|
||||
// Received a response
|
||||
$this->protocolReader->removeListener($listener);
|
||||
if (AdvancedJsonRpc\SuccessResponse::isSuccessResponse($msg->body)) {
|
||||
$resolve($msg->body->result);
|
||||
} else {
|
||||
$reject($msg->body->error);
|
||||
}
|
||||
}
|
||||
};
|
||||
$this->protocolReader->on('message', $listener);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a notification to the client
|
||||
*
|
||||
* @param string $method The method to call
|
||||
* @param array|object $params The method parameters
|
||||
* @return Promise <null> Will be resolved as soon as the notification has been sent
|
||||
*/
|
||||
public function notify(string $method, $params): Promise
|
||||
{
|
||||
$id = $this->idGenerator->generate();
|
||||
return $this->protocolWriter->write(
|
||||
new Protocol\Message(
|
||||
new AdvancedJsonRpc\Notification($method, (object)$params)
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,25 @@
|
|||
<?php
|
||||
declare(strict_types = 1);
|
||||
|
||||
namespace LanguageServer;
|
||||
|
||||
/**
|
||||
* Generates unique, incremental IDs for use as request IDs
|
||||
*/
|
||||
class IdGenerator
|
||||
{
|
||||
/**
|
||||
* @var int
|
||||
*/
|
||||
public $counter = 1;
|
||||
|
||||
/**
|
||||
* Returns a unique ID
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function generate()
|
||||
{
|
||||
return $this->counter++;
|
||||
}
|
||||
}
|
|
@ -3,9 +3,6 @@ declare(strict_types = 1);
|
|||
|
||||
namespace LanguageServer;
|
||||
|
||||
use LanguageServer\Client\TextDocument;
|
||||
use LanguageServer\Client\Window;
|
||||
|
||||
class LanguageClient
|
||||
{
|
||||
/**
|
||||
|
@ -22,12 +19,11 @@ class LanguageClient
|
|||
*/
|
||||
public $window;
|
||||
|
||||
private $protocolWriter;
|
||||
|
||||
public function __construct(ProtocolWriter $writer)
|
||||
public function __construct(ProtocolReader $reader, ProtocolWriter $writer)
|
||||
{
|
||||
$this->protocolWriter = $writer;
|
||||
$this->textDocument = new TextDocument($writer);
|
||||
$this->window = new Window($writer);
|
||||
$handler = new ClientHandler($reader, $writer);
|
||||
|
||||
$this->textDocument = new Client\TextDocument($handler);
|
||||
$this->window = new Client\Window($handler);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,7 +3,6 @@ declare(strict_types = 1);
|
|||
|
||||
namespace LanguageServer;
|
||||
|
||||
use LanguageServer\Server\TextDocument;
|
||||
use LanguageServer\Protocol\{
|
||||
ServerCapabilities,
|
||||
ClientCapabilities,
|
||||
|
@ -15,7 +14,6 @@ use LanguageServer\Protocol\{
|
|||
};
|
||||
use AdvancedJsonRpc;
|
||||
use Sabre\Event\Loop;
|
||||
use JsonMapper;
|
||||
use Exception;
|
||||
use Throwable;
|
||||
|
||||
|
@ -56,7 +54,11 @@ class LanguageServer extends AdvancedJsonRpc\Dispatcher
|
|||
{
|
||||
parent::__construct($this, '/');
|
||||
$this->protocolReader = $reader;
|
||||
$this->protocolReader->onMessage(function (Message $msg) {
|
||||
$this->protocolReader->on('message', function (Message $msg) {
|
||||
// Ignore responses, this is the handler for requests and notifications
|
||||
if (AdvancedJsonRpc\Response::isResponse($msg->body)) {
|
||||
return;
|
||||
}
|
||||
$result = null;
|
||||
$error = null;
|
||||
try {
|
||||
|
@ -81,7 +83,7 @@ class LanguageServer extends AdvancedJsonRpc\Dispatcher
|
|||
}
|
||||
});
|
||||
$this->protocolWriter = $writer;
|
||||
$this->client = new LanguageClient($writer);
|
||||
$this->client = new LanguageClient($reader, $writer);
|
||||
|
||||
$this->project = new Project($this->client);
|
||||
|
||||
|
|
|
@ -3,7 +3,13 @@ declare(strict_types = 1);
|
|||
|
||||
namespace LanguageServer;
|
||||
|
||||
interface ProtocolReader
|
||||
use Sabre\Event\EmitterInterface;
|
||||
|
||||
/**
|
||||
* Must emit a "message" event with a Protocol\Message object as parameter
|
||||
* when a message comes in
|
||||
*/
|
||||
interface ProtocolReader extends EmitterInterface
|
||||
{
|
||||
public function onMessage(callable $listener);
|
||||
|
||||
}
|
||||
|
|
|
@ -5,9 +5,9 @@ namespace LanguageServer;
|
|||
|
||||
use LanguageServer\Protocol\Message;
|
||||
use AdvancedJsonRpc\Message as MessageBody;
|
||||
use Sabre\Event\Loop;
|
||||
use Sabre\Event\{Loop, Emitter};
|
||||
|
||||
class ProtocolStreamReader implements ProtocolReader
|
||||
class ProtocolStreamReader extends Emitter implements ProtocolReader
|
||||
{
|
||||
const PARSE_HEADERS = 1;
|
||||
const PARSE_BODY = 2;
|
||||
|
@ -17,7 +17,6 @@ class ProtocolStreamReader implements ProtocolReader
|
|||
private $buffer = '';
|
||||
private $headers = [];
|
||||
private $contentLength;
|
||||
private $listener;
|
||||
|
||||
/**
|
||||
* @param resource $input
|
||||
|
@ -43,11 +42,8 @@ class ProtocolStreamReader implements ProtocolReader
|
|||
break;
|
||||
case self::PARSE_BODY:
|
||||
if (strlen($this->buffer) === $this->contentLength) {
|
||||
if (isset($this->listener)) {
|
||||
$msg = new Message(MessageBody::parse($this->buffer), $this->headers);
|
||||
$listener = $this->listener;
|
||||
$listener($msg);
|
||||
}
|
||||
$this->emit('message', [$msg]);
|
||||
$this->parsingMode = self::PARSE_HEADERS;
|
||||
$this->headers = [];
|
||||
$this->buffer = '';
|
||||
|
@ -57,13 +53,4 @@ class ProtocolStreamReader implements ProtocolReader
|
|||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param callable $listener Is called with a Message object
|
||||
* @return void
|
||||
*/
|
||||
public function onMessage(callable $listener)
|
||||
{
|
||||
$this->listener = $listener;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -31,12 +31,9 @@ class ProtocolStreamWriter implements ProtocolWriter
|
|||
}
|
||||
|
||||
/**
|
||||
* Sends a Message to the client
|
||||
*
|
||||
* @param Message $msg
|
||||
* @return Promise Resolved when the message has been fully written out to the output stream
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function write(Message $msg)
|
||||
public function write(Message $msg): Promise
|
||||
{
|
||||
// if the message queue is currently empty, register a write handler.
|
||||
if (empty($this->messages)) {
|
||||
|
|
|
@ -4,8 +4,15 @@ declare(strict_types = 1);
|
|||
namespace LanguageServer;
|
||||
|
||||
use LanguageServer\Protocol\Message;
|
||||
use Sabre\Event\Promise;
|
||||
|
||||
interface ProtocolWriter
|
||||
{
|
||||
public function write(Message $msg);
|
||||
/**
|
||||
* Sends a Message to the client
|
||||
*
|
||||
* @param Message $msg
|
||||
* @return Promise Resolved when the message has been fully written out to the output stream
|
||||
*/
|
||||
public function write(Message $msg): Promise;
|
||||
}
|
||||
|
|
|
@ -16,7 +16,7 @@ class LanguageServerTest extends TestCase
|
|||
$writer = new MockProtocolStream();
|
||||
$server = new LanguageServer($reader, $writer);
|
||||
$msg = null;
|
||||
$writer->onMessage(function (Message $message) use (&$msg) {
|
||||
$writer->on('message', function (Message $message) use (&$msg) {
|
||||
$msg = $message;
|
||||
});
|
||||
$reader->write(new Message(new AdvancedJsonRpc\Request(1, 'initialize', [
|
||||
|
@ -24,7 +24,7 @@ class LanguageServerTest extends TestCase
|
|||
'processId' => getmypid(),
|
||||
'capabilities' => new ClientCapabilities()
|
||||
])));
|
||||
$this->assertNotNull($msg, 'onMessage callback should be called');
|
||||
$this->assertNotNull($msg, 'message event should be emitted');
|
||||
$this->assertInstanceOf(AdvancedJsonRpc\SuccessResponse::class, $msg->body);
|
||||
$this->assertEquals((object)[
|
||||
'capabilities' => (object)[
|
||||
|
|
|
@ -5,34 +5,22 @@ namespace LanguageServer\Tests;
|
|||
|
||||
use LanguageServer\{ProtocolReader, ProtocolWriter};
|
||||
use LanguageServer\Protocol\Message;
|
||||
use Sabre\Event\{Emitter, Promise};
|
||||
|
||||
/**
|
||||
* A fake duplex protocol stream
|
||||
*/
|
||||
class MockProtocolStream implements ProtocolReader, ProtocolWriter
|
||||
class MockProtocolStream extends Emitter implements ProtocolReader, ProtocolWriter
|
||||
{
|
||||
private $listener;
|
||||
|
||||
/**
|
||||
* Sends a Message to the client
|
||||
*
|
||||
* @param Message $msg
|
||||
* @return void
|
||||
*/
|
||||
public function write(Message $msg)
|
||||
public function write(Message $msg): Promise
|
||||
{
|
||||
if (isset($this->listener)) {
|
||||
$listener = $this->listener;
|
||||
$listener(Message::parse((string)$msg));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param callable $listener Is called with a Message object
|
||||
* @return void
|
||||
*/
|
||||
public function onMessage(callable $listener)
|
||||
{
|
||||
$this->listener = $listener;
|
||||
$this->emit('message', [Message::parse((string)$msg)]);
|
||||
return Promise\resolve(null);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -15,7 +15,7 @@ class DefinitionCollectorTest extends TestCase
|
|||
{
|
||||
public function testCollectsSymbols()
|
||||
{
|
||||
$client = new LanguageClient(new MockProtocolStream());
|
||||
$client = new LanguageClient(new MockProtocolStream, new MockProtocolStream);
|
||||
$project = new Project($client);
|
||||
$parser = new Parser;
|
||||
$uri = pathToUri(realpath(__DIR__ . '/../../fixtures/symbols.php'));
|
||||
|
@ -54,7 +54,7 @@ class DefinitionCollectorTest extends TestCase
|
|||
|
||||
public function testDoesNotCollectReferences()
|
||||
{
|
||||
$client = new LanguageClient(new MockProtocolStream());
|
||||
$client = new LanguageClient(new MockProtocolStream, new MockProtocolStream);
|
||||
$project = new Project($client);
|
||||
$parser = new Parser;
|
||||
$uri = pathToUri(realpath(__DIR__ . '/../../fixtures/references.php'));
|
||||
|
|
|
@ -19,7 +19,7 @@ class PhpDocumentTest extends TestCase
|
|||
|
||||
public function setUp()
|
||||
{
|
||||
$this->project = new Project(new LanguageClient(new MockProtocolStream()));
|
||||
$this->project = new Project(new LanguageClient(new MockProtocolStream, new MockProtocolStream));
|
||||
}
|
||||
|
||||
public function testParsesVariableVariables()
|
||||
|
|
|
@ -19,7 +19,7 @@ class ProjectTest extends TestCase
|
|||
|
||||
public function setUp()
|
||||
{
|
||||
$this->project = new Project(new LanguageClient(new MockProtocolStream()));
|
||||
$this->project = new Project(new LanguageClient(new MockProtocolStream, new MockProtocolStream));
|
||||
}
|
||||
|
||||
public function testGetDocumentLoadsDocument()
|
||||
|
|
|
@ -18,7 +18,7 @@ class ProtocolStreamReaderTest extends TestCase
|
|||
$writeHandle = fopen($tmpfile, 'w');
|
||||
$reader = new ProtocolStreamReader(fopen($tmpfile, 'r'));
|
||||
$msg = null;
|
||||
$reader->onMessage(function (Message $message) use (&$msg) {
|
||||
$reader->on('message', function (Message $message) use (&$msg) {
|
||||
$msg = $message;
|
||||
});
|
||||
$ret = fwrite($writeHandle, (string)new Message(new RequestBody(1, 'aMethod', ['arg' => 'Hello World'])));
|
||||
|
|
|
@ -42,7 +42,7 @@ abstract class ServerTestCase extends TestCase
|
|||
|
||||
public function setUp()
|
||||
{
|
||||
$client = new LanguageClient(new MockProtocolStream());
|
||||
$client = new LanguageClient(new MockProtocolStream, new MockProtocolStream);
|
||||
$this->project = new Project($client);
|
||||
$this->textDocument = new Server\TextDocument($this->project, $client);
|
||||
$this->workspace = new Server\Workspace($this->project, $client);
|
||||
|
|
|
@ -12,7 +12,7 @@ class GlobalFallbackTest extends ServerTestCase
|
|||
{
|
||||
public function setUp()
|
||||
{
|
||||
$client = new LanguageClient(new MockProtocolStream());
|
||||
$client = new LanguageClient(new MockProtocolStream, new MockProtocolStream);
|
||||
$project = new Project($client);
|
||||
$this->textDocument = new Server\TextDocument($project, $client);
|
||||
$project->openDocument('global_fallback', file_get_contents(__DIR__ . '/../../../../fixtures/global_fallback.php'));
|
||||
|
|
|
@ -19,7 +19,7 @@ class DidChangeTest extends TestCase
|
|||
{
|
||||
public function test()
|
||||
{
|
||||
$client = new LanguageClient(new MockProtocolStream());
|
||||
$client = new LanguageClient(new MockProtocolStream, new MockProtocolStream);
|
||||
$project = new Project($client);
|
||||
$textDocument = new Server\TextDocument($project, $client);
|
||||
$phpDocument = $project->openDocument('whatever', "<?php\necho 'Hello, World'\n");
|
||||
|
|
|
@ -13,7 +13,7 @@ class DidCloseTest extends TestCase
|
|||
{
|
||||
public function test()
|
||||
{
|
||||
$client = new LanguageClient(new MockProtocolStream());
|
||||
$client = new LanguageClient(new MockProtocolStream, new MockProtocolStream);
|
||||
$project = new Project($client);
|
||||
$textDocument = new Server\TextDocument($project, $client);
|
||||
$phpDocument = $project->openDocument('whatever', 'hello world');
|
||||
|
|
|
@ -18,14 +18,14 @@ class FormattingTest extends TestCase
|
|||
|
||||
public function setUp()
|
||||
{
|
||||
$client = new LanguageClient(new MockProtocolStream());
|
||||
$client = new LanguageClient(new MockProtocolStream, new MockProtocolStream);
|
||||
$project = new Project($client);
|
||||
$this->textDocument = new Server\TextDocument($project, $client);
|
||||
}
|
||||
|
||||
public function testFormatting()
|
||||
{
|
||||
$client = new LanguageClient(new MockProtocolStream());
|
||||
$client = new LanguageClient(new MockProtocolStream, new MockProtocolStream);
|
||||
$project = new Project($client);
|
||||
$textDocument = new Server\TextDocument($project, $client);
|
||||
$path = realpath(__DIR__ . '/../../../fixtures/format.php');
|
||||
|
|
|
@ -5,8 +5,9 @@ namespace LanguageServer\Tests\Server\TextDocument;
|
|||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use LanguageServer\Tests\MockProtocolStream;
|
||||
use LanguageServer\{Server, Client, LanguageClient, Project};
|
||||
use LanguageServer\{Server, Client, LanguageClient, Project, ClientHandler};
|
||||
use LanguageServer\Protocol\{TextDocumentIdentifier, TextDocumentItem, DiagnosticSeverity};
|
||||
use Sabre\Event\Promise;
|
||||
|
||||
class ParseErrorsTest extends TestCase
|
||||
{
|
||||
|
@ -19,17 +20,18 @@ class ParseErrorsTest extends TestCase
|
|||
|
||||
public function setUp()
|
||||
{
|
||||
$client = new LanguageClient(new MockProtocolStream());
|
||||
$client = new LanguageClient(new MockProtocolStream, new MockProtocolStream);
|
||||
$client->textDocument = new class($this->args) extends Client\TextDocument {
|
||||
private $args;
|
||||
public function __construct(&$args)
|
||||
{
|
||||
parent::__construct(new MockProtocolStream());
|
||||
parent::__construct(new ClientHandler(new MockProtocolStream, new MockProtocolStream));
|
||||
$this->args = &$args;
|
||||
}
|
||||
public function publishDiagnostics(string $uri, array $diagnostics)
|
||||
public function publishDiagnostics(string $uri, array $diagnostics): Promise
|
||||
{
|
||||
$this->args = func_get_args();
|
||||
return Promise\resolve(null);
|
||||
}
|
||||
};
|
||||
$project = new Project($client);
|
||||
|
|
|
@ -13,7 +13,7 @@ class GlobalFallbackTest extends ServerTestCase
|
|||
{
|
||||
public function setUp()
|
||||
{
|
||||
$client = new LanguageClient(new MockProtocolStream());
|
||||
$client = new LanguageClient(new MockProtocolStream, new MockProtocolStream);
|
||||
$project = new Project($client);
|
||||
$this->textDocument = new Server\TextDocument($project, $client);
|
||||
$project->openDocument('global_fallback', file_get_contents(__DIR__ . '/../../../../fixtures/global_fallback.php'));
|
||||
|
|
Loading…
Reference in New Issue