Skip to content

Commit bacb8a4

Browse files
authored
Bring FileNode to 1.x (#9)
* [fix] Split test-related namespace to autoload-dev stanza * README updates * [Feature] Implement `FileNode` (#8) * Support `Stringable` as rule entries * Implement `FileNode` * Docs for `FileNode` * PHPStan fix
1 parent 30b9cb3 commit bacb8a4

11 files changed

+608
-6
lines changed
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
## File Upload Validation - Images
2+
3+
Here are a few examples for how you can validate image uploads.
4+
5+
### Validate dimensions
6+
7+
```php
8+
9+
$builder = Hyrule::create()
10+
->file('avatar')
11+
->required()
12+
->image() // Validates that upload is an image.
13+
->dimensions() // Starts dimension constraints...
14+
->ratio(1)
15+
->maxWidth(1000)
16+
->end() // Ends dimension rule-set.
17+
->end() // Ends the "avatar" field.
18+
// ...
19+
20+
```
21+
22+
See [`Dimensions`](https://github.com/laravel/framework/blob/9.x/src/Illuminate/Validation/Rules/Dimensions.php) class for all available constraints.
23+
24+
### Only accept subset of image types
25+
26+
```php
27+
$builder = Hyrule::create()
28+
->file('avatar')
29+
->required()
30+
->mimeType() // Starts MIME-type constriants...
31+
->image('jpeg', 'gif', 'png') // Only accept image/{jpeg,gif,png}
32+
->end() // End MIME-Type constraints.
33+
->end() // End the "avatar" field.
34+
// ...
35+
```
36+
37+
See [File Upload Validation - MIME Types](./file-upload-validation-mime-types.md) for a comprehensive guide on MIME-Type rules.
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
## File Upload Validation - MIME Types
2+
3+
It is considered best practice to validate the MIME type of uploaded files. Here are a few examples on how to do that with Hyrule:
4+
5+
```php
6+
$builder = Hyrule::create()
7+
->file('attachment')
8+
->mimeType() // Starts MIME-Type contraints
9+
/*
10+
* All 5 top-level MIME type categories are supported
11+
*/
12+
->application('pdf') // Allows application/pdf
13+
->image('jpg', 'png', ...) // Variadic. Enumerate sub-types e.g. image/jpeg, image/png, etc.
14+
->video('mp4', 'webm')
15+
->multipart(...)
16+
->message(...)
17+
->end() // Ends MIME Type constraint.
18+
->end() // Ends "attachment" field
19+
// ...
20+
```
21+
22+
Use `->allow(...)` to enumerate specific specific MIME-types:
23+
24+
```php
25+
$builder = Hyrule::create()
26+
->array('attachments')
27+
->between(1, 10)
28+
->each('file')
29+
->mimeType()
30+
->allow('application/pdf')
31+
->allow('image/jpg')
32+
->allow('image/png')
33+
->allow('image/svg')
34+
->allow('video/mp4')
35+
// etc.
36+
->end()
37+
->end()
38+
->end()
39+
// ...
40+
41+
```

src/Nodes/AbstractNode.php

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -166,9 +166,9 @@ public function requiredWith($path): self
166166
*/
167167
public function rule($rule): self
168168
{
169-
if (!is_string($rule) && !$rule instanceof Rule) {
169+
if (!is_string($rule) && !$rule instanceof Rule && !method_exists($rule, '__toString')) {
170170
throw new InvalidArgumentException(sprintf(
171-
'Expected argument to be a string, or instance of %s.', Rule::class,
171+
'Expected argument to be a string, instance of %s, or has a __toString() method.', Rule::class,
172172
));
173173
}
174174
$this->rules[] = $rule;
@@ -234,6 +234,8 @@ public function build(): array
234234
$rule = (string) $rule->setNode($this)->stringify();
235235
} else if ($rule instanceof self) {
236236
$rule = (string) (new Path($rule))->pathName();
237+
} else if (method_exists($rule, '__toString')) {
238+
$rule = (string) $rule;
237239
}
238240
return $rule;
239241
}, $this->rules);

src/Nodes/ArrayNode.php

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,6 @@
99
use InvalidArgumentException;
1010
use LogicException;
1111

12-
/**
13-
* @property bool $allowUnknownProperties
14-
*/
1512
class ArrayNode extends CompoundNode
1613
{
1714
/**
@@ -31,6 +28,7 @@ class ArrayNode extends CompoundNode
3128
'array' => ArrayNode::class,
3229
'object' => ObjectNode::class,
3330
'scalar' => ScalarNode::class,
31+
'file' => FileNode::class,
3432
];
3533

3634
/**
@@ -45,7 +43,7 @@ public function __construct(string $name, ?CompoundNode $parent = null)
4543

4644
/**
4745
* @param string $type
48-
* @return AbstractNode|ArrayNode|ObjectNode
46+
* @return AbstractNode|ArrayNode|ObjectNode|FileNode
4947
*/
5048
public function each(string $type): AbstractNode
5149
{

src/Nodes/FileNode.php

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
<?php
2+
3+
namespace Square\Hyrule\Nodes;
4+
5+
use Square\Hyrule\Rules\Dimensions;
6+
use Square\Hyrule\Rules\MIMEType;
7+
8+
class FileNode extends AbstractNode
9+
{
10+
protected ?Dimensions $dimensions;
11+
12+
protected ?MIMEType $mimeType;
13+
14+
/**
15+
* @return Dimensions
16+
*/
17+
public function dimensions(): Dimensions
18+
{
19+
if (!isset($this->dimensions)) {
20+
// Create a new Dimensions rule object, push it to the rules array, and
21+
// keep a reference so we can modify it when we need to in the future.
22+
$this->rule($this->dimensions = new Dimensions($this));
23+
}
24+
return $this->dimensions;
25+
}
26+
27+
/**
28+
* @return $this
29+
*/
30+
public function image(): FileNode
31+
{
32+
$this->rule('image');
33+
return $this;
34+
}
35+
36+
/**
37+
* @return MIMEType
38+
*/
39+
public function mimeType(): MIMEType
40+
{
41+
if (!isset($this->mimeType)) {
42+
// Create a new MIMEType rule object, push it to the rules array, and
43+
// keep a reference so we can modify it when we need to in the future.
44+
$this->rule($this->mimeType = new MIMEType($this));
45+
}
46+
return $this->mimeType;
47+
}
48+
}

src/Nodes/ObjectNode.php

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,15 @@ public function array(string $name): ArrayNode
8888
return $this->registerNode(new ArrayNode($name, $this));
8989
}
9090

91+
/**
92+
* @param string $name
93+
* @return FileNode
94+
*/
95+
public function file(string $name): FileNode
96+
{
97+
return $this->registerNode(new FileNode($name, $this));
98+
}
99+
91100
/**
92101
* @param string $name
93102
* @return ObjectNode
@@ -195,6 +204,18 @@ public function arrayWith(string $name, callable $callable): self
195204
return $this;
196205
}
197206

207+
/**
208+
* @param string $name
209+
* @param callable $callable
210+
* @return $this
211+
*/
212+
public function fileWith(string $name, callable $callable): self
213+
{
214+
$this->registerNode(new FileNode($name, $this))
215+
->with($callable);
216+
return $this;
217+
}
218+
198219
/**
199220
* @param string $name
200221
* @param callable $callable

src/PHPStan/ArrayNodeEachDynamicReturnExtension.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
use PHPStan\Type\Type;
1414
use Square\Hyrule\Nodes\ArrayNode;
1515
use Square\Hyrule\Nodes\BooleanNode;
16+
use Square\Hyrule\Nodes\FileNode;
1617
use Square\Hyrule\Nodes\FloatNode;
1718
use Square\Hyrule\Nodes\IntegerNode;
1819
use Square\Hyrule\Nodes\NumericNode;
@@ -65,6 +66,8 @@ public function getTypeFromMethodCall(MethodReflection $methodReflection, Method
6566
return new ObjectType(NumericNode::class);
6667
case "boolean":
6768
return new ObjectType(BooleanNode::class);
69+
case "file":
70+
return new ObjectType(FileNode::class);
6871
default:
6972
return null;
7073
}

src/Rules/Dimensions.php

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
<?php
2+
3+
namespace Square\Hyrule\Rules;
4+
5+
use Illuminate\Validation\Rules\Dimensions as DimensionsRule;
6+
use Square\Hyrule\Nodes\FileNode;
7+
8+
/**
9+
* Extends the Laravel's built-in Dimensions helper and adds methods to support Hyrule's fluent API.
10+
*/
11+
class Dimensions extends DimensionsRule
12+
{
13+
private FileNode $node;
14+
15+
/**
16+
* @param FileNode $node
17+
* @param array<string,mixed> $constraints
18+
*/
19+
public function __construct(FileNode $node, array $constraints = [])
20+
{
21+
$this->node = $node;
22+
parent::__construct($constraints);
23+
}
24+
25+
public function end(): FileNode
26+
{
27+
return $this->node;
28+
}
29+
}

0 commit comments

Comments
 (0)