Skip to content

Commit b8256ce

Browse files
committed
implement glob spec
1 parent ce5d9a3 commit b8256ce

File tree

6 files changed

+503
-1
lines changed

6 files changed

+503
-1
lines changed
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
<?php
2+
require_once(__DIR__ . '/../vendor/autoload.php');
3+
4+
use Flyfinder\Specification\Glob;
5+
use League\Flysystem\Filesystem;
6+
use League\Flysystem\Adapter\Local;
7+
use Flyfinder\Finder;
8+
use Flyfinder\Path;
9+
use Flyfinder\Specification\IsHidden;
10+
use Flyfinder\Specification\HasExtension;
11+
use Flyfinder\Specification\InPath;
12+
use Flyfinder\Specification\AndSpecification;
13+
14+
// (03-sample-files based on some phpDocumentor2 src files)
15+
$filesystem = new Filesystem(new Local(__DIR__ . '/03-sample-files'));
16+
$filesystem->addPlugin(new Finder());
17+
18+
/*
19+
* "phpdoc -d src -i src/phpDocumentor/DomainModel"
20+
* should result in src/Cilex and src/phpDocumentor/. files being found,
21+
* but src/phpDocumentor/DomainModel files being left out
22+
*/
23+
$dashDirectoryPath = new Glob('/src/**/*');
24+
$dashIgnorePath = new InPath(new Path('src/phpDocumentor/DomainModel'));
25+
$isHidden = new IsHidden();
26+
$isPhpFile = new HasExtension(['php']);
27+
$spec = new AndSpecification($dashDirectoryPath, $dashIgnorePath->notSpecification());
28+
$spec->andSpecification($isHidden->notSpecification());
29+
$spec->andSpecification($isPhpFile);
30+
31+
$generator = $filesystem->find($spec);
32+
$result = [];
33+
foreach($generator as $value) {
34+
$result[] = $value;
35+
}

phpcs.xml.dist

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
<rule ref="Generic.Files.LineLength.TooLong">
1818
<exclude-pattern>*/src/Finder.php</exclude-pattern>
1919
<exclude-pattern>*/src/Specification/SpecificationInterface.php</exclude-pattern>
20+
<exclude-pattern>*/tests/unit/Specification/GlobTest.php</exclude-pattern>
2021
</rule>
2122

2223

src/Specification/Glob.php

Lines changed: 261 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,261 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
/**
6+
* This file is part of phpDocumentor.
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*
11+
* Many thanks to webmozart by providing the original code in webmozart/glob
12+
*
13+
* @link https://github.com/webmozart/glob/blob/master/src/Glob.php
14+
* @link http://phpdoc.org
15+
*/
16+
17+
namespace Flyfinder\Specification;
18+
19+
use InvalidArgumentException;
20+
use function preg_match;
21+
use function sprintf;
22+
use function strlen;
23+
use function strpos;
24+
25+
/**
26+
* Glob specification class
27+
*
28+
* @psalm-immutable
29+
*/
30+
final class Glob extends CompositeSpecification
31+
{
32+
/** @var string */
33+
private $regex;
34+
35+
/**
36+
* The "static prefix" is the part of the glob up to the first wildcard "*".
37+
* If the glob does not contain wildcards, the full glob is returned.
38+
*
39+
* @var string
40+
*/
41+
private $staticPrefix;
42+
43+
public function __construct(string $glob)
44+
{
45+
$this->regex = self::toRegEx($glob);
46+
$this->staticPrefix = self::getStaticPrefix($glob);
47+
}
48+
49+
/**
50+
* @inheritDoc
51+
*/
52+
public function isSatisfiedBy(array $value) : bool
53+
{
54+
//Flysystem paths are not absolute, so make it that way.
55+
$path = '/' . $value['path'];
56+
if (strpos($path, $this->staticPrefix) !== 0) {
57+
return false;
58+
}
59+
60+
if (preg_match($this->regex, $path)) {
61+
return true;
62+
}
63+
64+
return false;
65+
}
66+
67+
/**
68+
* Returns the static prefix of a glob.
69+
*
70+
* The "static prefix" is the part of the glob up to the first wildcard "*".
71+
* If the glob does not contain wildcards, the full glob is returned.
72+
*
73+
* @param string $glob The canonical glob. The glob should contain forward
74+
* slashes as directory separators only. It must not
75+
* contain any "." or ".." segments.
76+
*
77+
* @return string The static prefix of the glob.
78+
*
79+
* @psalm-pure
80+
*/
81+
private static function getStaticPrefix(string $glob) : string
82+
{
83+
if (strpos($glob, '/') !== 0 && strpos($glob, '://') === false) {
84+
throw new InvalidArgumentException(sprintf(
85+
'The glob "%s" is not absolute and not a URI.',
86+
$glob
87+
));
88+
}
89+
$prefix = '';
90+
$length = strlen($glob);
91+
for ($i = 0; $i < $length; ++$i) {
92+
$c = $glob[$i];
93+
switch ($c) {
94+
case '/':
95+
$prefix .= '/';
96+
if (self::isRecursiveWildcard($glob, $i)) {
97+
break 2;
98+
}
99+
break;
100+
case '*':
101+
case '?':
102+
case '{':
103+
case '[':
104+
break 2;
105+
case '\\':
106+
if (isset($glob[$i + 1])) {
107+
switch ($glob[$i + 1]) {
108+
case '*':
109+
case '?':
110+
case '{':
111+
case '}':
112+
case '[':
113+
case ']':
114+
case '-':
115+
case '^':
116+
case '$':
117+
case '~':
118+
case '\\':
119+
$prefix .= $glob[$i + 1];
120+
++$i;
121+
break;
122+
default:
123+
$prefix .= '\\';
124+
}
125+
}
126+
break;
127+
default:
128+
$prefix .= $c;
129+
break;
130+
}
131+
}
132+
return $prefix;
133+
}
134+
135+
/**
136+
* Checks if the current position the glob is start of a Recursive directory wildcard
137+
*
138+
* @psalm-pure
139+
*/
140+
private static function isRecursiveWildcard(string $glob, int $i) : bool
141+
{
142+
return isset($glob[$i + 3]) && $glob[$i + 1] . $glob[$i + 2] . $glob[$i + 3] === '**/';
143+
}
144+
145+
/**
146+
* Converts a glob to a regular expression.
147+
*
148+
* @param string $glob The canonical glob. The glob should contain forward
149+
* slashes as directory separators only. It must not
150+
* contain any "." or ".." segments.
151+
*
152+
* @return string The regular expression for matching the glob.
153+
*
154+
* @psalm-pure
155+
*/
156+
private static function toRegEx(string $glob) : string
157+
{
158+
$delimiter = '~';
159+
$inSquare = false;
160+
$curlyLevels = 0;
161+
$regex = '';
162+
$length = strlen($glob);
163+
for ($i = 0; $i < $length; ++$i) {
164+
$c = $glob[$i];
165+
switch ($c) {
166+
case '.':
167+
case '(':
168+
case ')':
169+
case '|':
170+
case '+':
171+
case '^':
172+
case '$':
173+
case $delimiter:
174+
$regex .= '\\' . $c;
175+
break;
176+
case '/':
177+
if (self::isRecursiveWildcard($glob, $i)) {
178+
$regex .= '/([^/]+/)*';
179+
$i += 3;
180+
} else {
181+
$regex .= '/';
182+
}
183+
break;
184+
case '*':
185+
$regex .= '[^/]*';
186+
break;
187+
case '?':
188+
$regex .= '.';
189+
break;
190+
case '{':
191+
$regex .= '(';
192+
++$curlyLevels;
193+
break;
194+
case '}':
195+
if ($curlyLevels > 0) {
196+
$regex .= ')';
197+
--$curlyLevels;
198+
} else {
199+
$regex .= '}';
200+
}
201+
break;
202+
case ',':
203+
$regex .= $curlyLevels > 0 ? '|' : ',';
204+
break;
205+
case '[':
206+
$regex .= '[';
207+
$inSquare = true;
208+
if (isset($glob[$i + 1]) && $glob[$i + 1] === '^') {
209+
$regex .= '^';
210+
++$i;
211+
}
212+
break;
213+
case ']':
214+
$regex .= $inSquare ? ']' : '\\]';
215+
$inSquare = false;
216+
break;
217+
case '-':
218+
$regex .= $inSquare ? '-' : '\\-';
219+
break;
220+
case '\\':
221+
if (isset($glob[$i + 1])) {
222+
switch ($glob[$i + 1]) {
223+
case '*':
224+
case '?':
225+
case '{':
226+
case '}':
227+
case '[':
228+
case ']':
229+
case '-':
230+
case '^':
231+
case '$':
232+
case '~':
233+
case '\\':
234+
$regex .= '\\' . $glob[$i + 1];
235+
++$i;
236+
break;
237+
default:
238+
$regex .= '\\\\';
239+
}
240+
}
241+
break;
242+
default:
243+
$regex .= $c;
244+
break;
245+
}
246+
}
247+
if ($inSquare) {
248+
throw new InvalidArgumentException(sprintf(
249+
'Invalid glob: missing ] in %s',
250+
$glob
251+
));
252+
}
253+
if ($curlyLevels > 0) {
254+
throw new InvalidArgumentException(sprintf(
255+
'Invalid glob: missing } in %s',
256+
$glob
257+
));
258+
}
259+
return $delimiter . '^' . $regex . '$' . $delimiter;
260+
}
261+
}

tests/integration/FindOnSamplePhpdocLayout.php renamed to tests/integration/FindOnSamplePhpdocLayoutTest.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
*
2121
* @coversNothing
2222
*/
23-
class FindOnSamplePhpdocLayout extends TestCase
23+
class FindOnSamplePhpdocLayoutTest extends TestCase
2424
{
2525
public function testFindingOnSamplePhpdocLayout() : void
2626
{
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
/**
6+
* This file is part of phpDocumentor.
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*
11+
* @link http://phpdoc.org
12+
*/
13+
14+
namespace Flyfinder;
15+
16+
use PHPUnit\Framework\TestCase;
17+
18+
/**
19+
* Integration test against examples/04-sample-phpdoc-layout-using-glob.php
20+
*
21+
* @coversNothing
22+
*/
23+
class FindOnSamplePhpdocLayoutUsingGlobTest extends TestCase
24+
{
25+
public function testFindingOnSamplePhpdocLayout() : void
26+
{
27+
$result = [];
28+
include __DIR__ . '/../../examples/04-sample-phpdoc-layout-using-glob.php';
29+
30+
$this->assertCount(4, $result);
31+
$this->assertSame('JmsSerializerServiceProvider.php', $result[0]['basename']);
32+
$this->assertSame('MonologServiceProvider.php', $result[1]['basename']);
33+
$this->assertSame('Application.php', $result[2]['basename']);
34+
$this->assertSame('Bootstrap.php', $result[3]['basename']);
35+
}
36+
}

0 commit comments

Comments
 (0)