Skip to content

Commit 31ff399

Browse files
authored
fix: accept any callable of the correct shape as a format reader (#29)
1 parent ef04159 commit 31ff399

7 files changed

Lines changed: 173 additions & 12 deletions

File tree

phpstan.neon.dist

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,4 @@ parameters:
99
- */tests/*/fixtures/*
1010
ignoreErrors:
1111
- '#Cannot call method getName\(\) on ReflectionType\|null#'
12+
- '#Method FormatPHP\\Util\\FormatHelper::loadFormatter\(\) never returns callable\(array\): FormatPHP\\MessageCollection so it can be removed from the return type#'

src/MessageLoader.php

Lines changed: 44 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,45 +22,60 @@
2222

2323
namespace FormatPHP;
2424

25+
use FormatPHP\Exception\ImproperContextException;
2526
use FormatPHP\Exception\InvalidArgumentException;
2627
use FormatPHP\Exception\InvalidMessageShapeException;
2728
use FormatPHP\Exception\LocaleNotFoundException;
2829
use FormatPHP\Exception\UnableToProcessFileException;
2930
use FormatPHP\Format\Reader\FormatPHPReader;
3031
use FormatPHP\Format\ReaderInterface;
3132
use FormatPHP\Util\FileSystemHelper;
33+
use FormatPHP\Util\FormatHelper;
3234

3335
use function array_filter;
3436
use function array_unique;
3537
use function array_values;
3638
use function implode;
39+
use function is_callable;
3740
use function sprintf;
3841

3942
use const DIRECTORY_SEPARATOR;
4043

4144
/**
4245
* Loads messages for a given locale from the file system or cache
46+
*
47+
* @psalm-import-type ReaderType from ReaderInterface
4348
*/
4449
class MessageLoader
4550
{
4651
private ConfigInterface $config;
4752
private FileSystemHelper $fileSystemHelper;
48-
private ReaderInterface $formatReader;
53+
private FormatHelper $formatHelper;
4954
private string $messagesDirectory;
5055

5156
/**
57+
* @var ReaderType
58+
*/
59+
private $formatReader;
60+
61+
/**
62+
* @param ReaderType | string | null $formatReader
63+
*
5264
* @throws InvalidArgumentException
65+
* @throws ImproperContextException
5366
*/
5467
public function __construct(
5568
string $messagesDirectory,
5669
ConfigInterface $config,
57-
?ReaderInterface $formatReader = null,
58-
?FileSystemHelper $fileSystemHelper = null
70+
$formatReader = null,
71+
?FileSystemHelper $fileSystemHelper = null,
72+
?FormatHelper $formatHelper = null
5973
) {
6074
$this->config = $config;
61-
$this->formatReader = $formatReader ?? new FormatPHPReader();
6275
$this->fileSystemHelper = $fileSystemHelper ?? new FileSystemHelper();
76+
$this->formatHelper = $formatHelper ?? new FormatHelper($this->fileSystemHelper);
6377
$this->messagesDirectory = $this->fileSystemHelper->getRealPath($messagesDirectory);
78+
$this->formatReader = $this->loadFormatReader($formatReader);
6479

6580
if (!$this->fileSystemHelper->isDirectory($this->messagesDirectory)) {
6681
throw new InvalidArgumentException(sprintf(
@@ -131,4 +146,29 @@ private function getFallbackLocales(): array
131146
/** @var string[] */
132147
return array_values(array_unique(array_filter($fallbacks)));
133148
}
149+
150+
/**
151+
* @param ReaderType | string | null $formatReader
152+
*
153+
* @return ReaderType
154+
*
155+
* @throws ImproperContextException
156+
* @throws InvalidArgumentException
157+
*/
158+
private function loadFormatReader($formatReader): callable
159+
{
160+
if ($formatReader === null) {
161+
return new FormatPHPReader();
162+
}
163+
164+
if ($formatReader instanceof ReaderInterface) {
165+
return $formatReader;
166+
}
167+
168+
if (is_callable($formatReader)) {
169+
return $this->formatHelper->validateReaderCallable($formatReader);
170+
}
171+
172+
return $this->formatHelper->getReader($formatReader);
173+
}
134174
}

src/Util/FormatHelper.php

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -132,7 +132,7 @@ public function getWriter(?string $format): callable
132132
/**
133133
* @param class-string<ReaderInterface> | class-string<WriterInterface> $type
134134
*
135-
* @return ReaderCallableType | WriterCallableType
135+
* @return ReaderType | WriterType
136136
*
137137
* @throws ImproperContextException
138138
* @throws InvalidArgumentException
@@ -147,24 +147,30 @@ private function loadFormatter(string $format, string $type): callable
147147
$formatter = $this->fileSystemHelper->loadClosureFromScript($format);
148148

149149
if ($type === ReaderInterface::class) {
150-
$formatter = $this->validateReaderClosure($formatter);
150+
$formatter = $this->validateReaderCallable($formatter);
151151
} else {
152-
$formatter = $this->validateWriterClosure($formatter);
152+
$formatter = $this->validateWriterCallable($formatter);
153153
}
154154

155155
return $formatter;
156156
}
157157

158158
/**
159+
* @return ReaderCallableType
160+
*
159161
* @throws InvalidArgumentException
160162
*
161163
* @psalm-suppress UndefinedMethod, PossiblyNullReference
162164
*/
163-
private function validateReaderClosure(?Closure $formatter): Closure
165+
public function validateReaderCallable(?callable $formatter): callable
164166
{
165167
try {
166168
assert($formatter !== null);
167169

170+
if (!$formatter instanceof Closure) {
171+
$formatter = Closure::fromCallable($formatter);
172+
}
173+
168174
$reflected = new ReflectionFunction($formatter);
169175

170176
assert($reflected->getNumberOfParameters() === 1);
@@ -186,15 +192,21 @@ private function validateReaderClosure(?Closure $formatter): Closure
186192
}
187193

188194
/**
195+
* @return WriterCallableType
196+
*
189197
* @throws InvalidArgumentException
190198
*
191199
* @psalm-suppress UndefinedMethod, PossiblyNullReference
192200
*/
193-
private function validateWriterClosure(?Closure $formatter): Closure
201+
public function validateWriterCallable(?callable $formatter): callable
194202
{
195203
try {
196204
assert($formatter !== null);
197205

206+
if (!$formatter instanceof Closure) {
207+
$formatter = Closure::fromCallable($formatter);
208+
}
209+
198210
$reflected = new ReflectionFunction($formatter);
199211

200212
assert($reflected->getNumberOfParameters() === 2);
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace FormatPHP\Test;
6+
7+
use FormatPHP\Format\Reader\FormatPHPReader;
8+
9+
class CustomMessageLoaderReader extends FormatPHPReader
10+
{
11+
}

tests/MessageLoaderTest.php

Lines changed: 55 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,15 @@
1010
use FormatPHP\Format\Reader\FormatPHPReader;
1111
use FormatPHP\Format\ReaderInterface;
1212
use FormatPHP\Intl\Locale;
13+
use FormatPHP\MessageCollection;
1314
use FormatPHP\MessageInterface;
1415
use FormatPHP\MessageLoader;
1516

1617
use function sprintf;
1718

19+
/**
20+
* @psalm-import-type ReaderType from ReaderInterface
21+
*/
1822
class MessageLoaderTest extends TestCase
1923
{
2024
public function testExceptionWhenDirectoryIsNotValid(): void
@@ -25,10 +29,13 @@ public function testExceptionWhenDirectoryIsNotValid(): void
2529
__FILE__,
2630
));
2731

32+
/** @var ReaderInterface $reader */
33+
$reader = $this->mockery(ReaderInterface::class);
34+
2835
new MessageLoader(
2936
__FILE__,
3037
new Config(new Locale('en')),
31-
$this->mockery(ReaderInterface::class),
38+
$reader,
3239
);
3340
}
3441

@@ -37,10 +44,13 @@ public function testExceptionWhenUnableToFindSuitableLocale(): void
3744
// Esperanto, Latin script, US region.
3845
$locale = new Locale('eo-Latn-US');
3946

47+
/** @var ReaderInterface $reader */
48+
$reader = $this->mockery(ReaderInterface::class);
49+
4050
$loader = new MessageLoader(
4151
__DIR__ . '/fixtures/locales',
4252
new Config($locale),
43-
$this->mockery(ReaderInterface::class),
53+
$reader,
4454
);
4555

4656
$this->expectException(LocaleNotFoundException::class);
@@ -91,4 +101,47 @@ public function testLoadMessagesWithFallback(): void
91101
$collection['about.inspire']->getMessage(),
92102
);
93103
}
104+
105+
/**
106+
* @param ReaderType $customReader
107+
*
108+
* @dataProvider provideCustomReader
109+
*/
110+
public function testLoadMessagesWithCustomReader($customReader): void
111+
{
112+
$locale = new Locale('ar');
113+
114+
$loader = new MessageLoader(
115+
__DIR__ . '/fixtures/locales',
116+
new Config($locale),
117+
$customReader,
118+
);
119+
120+
$collection = $loader->loadMessages();
121+
122+
$this->assertCount(1, $collection);
123+
$this->assertNotNull($collection['about.inspire']);
124+
$this->assertInstanceOf(MessageInterface::class, $collection['about.inspire']);
125+
$this->assertSame(
126+
'في Skillshare ، نقوم بتمكين الأعضاء للحصول على الإلهام.',
127+
$collection['about.inspire']->getMessage(),
128+
);
129+
}
130+
131+
/**
132+
* @return mixed[]
133+
*/
134+
public function provideCustomReader(): array
135+
{
136+
$customReader = new CustomMessageLoaderReader();
137+
138+
return [
139+
['customReader' => CustomMessageLoaderReader::class],
140+
['customReader' => $customReader],
141+
['customReader' => fn (array $data): MessageCollection => (new FormatPHPReader())($data)],
142+
['customReader' => [$customReader, '__invoke']],
143+
['customReader' => __DIR__ . '/fixtures/custom-reader.php'],
144+
['customReader' => null],
145+
];
146+
}
94147
}

tests/Util/FormatHelperTest.php

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -150,7 +150,7 @@ public function testGetWriter(string $writer, string $expectedType): void
150150
}
151151

152152
/**
153-
* @return array<string, array{writer: string, expectedType: string}>
153+
* @return mixed[]
154154
*/
155155
public function validWriterProvider(): array
156156
{
@@ -235,4 +235,40 @@ public function invalidWriterProvider(): array
235235
],
236236
];
237237
}
238+
239+
/**
240+
* @dataProvider validateWriterCallableProvider
241+
*/
242+
public function testValidateWriterCallable(callable $writer): void
243+
{
244+
$helper = new FormatHelper(new FileSystemHelper());
245+
246+
$this->assertInstanceOf(Closure::class, $helper->validateWriterCallable($writer));
247+
}
248+
249+
/**
250+
* @return mixed[]
251+
*/
252+
public function validateWriterCallableProvider(): array
253+
{
254+
$writerInstance = new ChromeWriter();
255+
256+
return [
257+
'formatphp' => [
258+
'writer' => new FormatPHPWriter(),
259+
],
260+
'callable array' => [
261+
'writer' => [$writerInstance, '__invoke'],
262+
],
263+
'loaded closure' => [
264+
'writer' => require __DIR__ . '/fixtures/writer-closure-01.php',
265+
],
266+
'loaded anonymous class' => [
267+
'writer' => require __DIR__ . '/fixtures/writer-closure-02.php',
268+
],
269+
'closure' => [
270+
'writer' => fn (DescriptorCollection $descriptors, WriterOptions $options): array => [],
271+
],
272+
];
273+
}
238274
}

tests/fixtures/custom-reader.php

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
use FormatPHP\Format\Reader\FormatPHPReader;
6+
use FormatPHP\MessageCollection;
7+
8+
return fn (array $data): MessageCollection => (new FormatPHPReader())($data);

0 commit comments

Comments
 (0)