Skip to content

Commit

Permalink
[Feature] Implement FileNode (#8)
Browse files Browse the repository at this point in the history
* Support `Stringable` as rule entries
* Implement `FileNode`
* Docs for `FileNode`
  • Loading branch information
bezhermoso authored Jun 25, 2022
1 parent 7c76a65 commit 99995aa
Show file tree
Hide file tree
Showing 12 changed files with 629 additions and 8 deletions.
18 changes: 18 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -230,6 +230,24 @@ $builder
// etc.
```

### File Uploads

As of 2.3, Hyrule supports specifying rules for file uploads:

```php
$builder
->file('attachment')
->required()
->mime('image', 'video', 'text')
->end()
// etc.

```

See the following detailed guides on how to validate file uploads by file-type (MIME type), dimensions, etc.

* [File Upload Validation - Images](./docs/file-upload-validation-images.md)
* [File Upload Validation - Other MIME Types](./docs/file-upload-validation-mime-types.md)


## Rules API
Expand Down
37 changes: 37 additions & 0 deletions docs/file-upload-validation-images.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
## File Upload Validation - Images

Here are a few examples for how you can validate image uploads.

### Validate dimensions

```php

$builder = Hyrule::create()
->file('avatar')
->required()
->image() // Validates that upload is an image.
->dimensions() // Starts dimension constraints...
->ratio(1)
->maxWidth(1000)
->end() // Ends dimension rule-set.
->end() // Ends the "avatar" field.
// ...

```

See [`Dimensions`](https://github.com/laravel/framework/blob/9.x/src/Illuminate/Validation/Rules/Dimensions.php) class for all available constraints.

### Only accept subset of image types

```php
$builder = Hyrule::create()
->file('avatar')
->required()
->mimeType() // Starts MIME-type constriants...
->image('jpeg', 'gif', 'png') // Only accept image/{jpeg,gif,png}
->end() // End MIME-Type constraints.
->end() // End the "avatar" field.
// ...
```

See [File Upload Validation - MIME Types](./file-upload-validation-mime-types.md) for a comprehensive guide on MIME-Type rules.
41 changes: 41 additions & 0 deletions docs/file-upload-validation-mime-types.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
## File Upload Validation - MIME Types

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:

```php
$builder = Hyrule::create()
->file('attachment')
->mimeType() // Starts MIME-Type contraints
/*
* All 5 top-level MIME type categories are supported
*/
->application('pdf') // Allows application/pdf
->image('jpg', 'png', ...) // Variadic. Enumerate sub-types e.g. image/jpeg, image/png, etc.
->video('mp4', 'webm')
->multipart(...)
->message(...)
->end() // Ends MIME Type constraint.
->end() // Ends "attachment" field
// ...
```

Use `->allow(...)` to enumerate specific specific MIME-types:

```php
$builder = Hyrule::create()
->array('attachments')
->between(1, 10)
->each('file')
->mimeType()
->allow('application/pdf')
->allow('image/jpg')
->allow('image/png')
->allow('image/svg')
->allow('video/mp4')
// etc.
->end()
->end()
->end()
// ...

```
11 changes: 7 additions & 4 deletions src/Nodes/AbstractNode.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
use Square\Hyrule\Build\LazyRuleStringify;
use Square\Hyrule\Path;
use Square\Hyrule\PathExp;
use Stringable;

/**
* @method $this|AbstractNode|ArrayNode|ScalarNode|ObjectNode required()
Expand All @@ -28,7 +29,7 @@
abstract class AbstractNode
{
/**
* @var array|string[]|LazyRuleStringify[]|Rule[]
* @var array|string[]|LazyRuleStringify[]|Rule[]|Stringable[]
*/
protected array $rules = [];

Expand Down Expand Up @@ -137,10 +138,10 @@ public function requiredWith(string|PathExp|AbstractNode $path): self
}

/**
* @param string|Rule $rule
* @return $this|AbstractNode|ScalarNode|ObjectNode|ArrayNode
* @param string|Rule|Stringable $rule
* @return AbstractNode
*/
public function rule(string|Rule $rule): self
public function rule(string|Rule|Stringable $rule): self
{
$this->rules[] = $rule;
return $this;
Expand Down Expand Up @@ -205,6 +206,8 @@ public function build(): array
$rule = (string) $rule->setNode($this)->stringify();
} else if ($rule instanceof self) {
$rule = (string) (new Path($rule))->pathName();
} else if ($rule instanceof Stringable) {
$rule = $rule->__toString();
}
return $rule;
}, $this->rules);
Expand Down
6 changes: 2 additions & 4 deletions src/Nodes/ArrayNode.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,6 @@
use InvalidArgumentException;
use LogicException;

/**
* @property bool $allowUnknownProperties
*/
class ArrayNode extends CompoundNode
{
/**
Expand All @@ -31,6 +28,7 @@ class ArrayNode extends CompoundNode
'array' => ArrayNode::class,
'object' => ObjectNode::class,
'scalar' => ScalarNode::class,
'file' => FileNode::class,
];

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

/**
* @param string $type
* @return AbstractNode|ArrayNode|ObjectNode
* @return AbstractNode|ArrayNode|ObjectNode|FileNode
*/
public function each(string $type): AbstractNode
{
Expand Down
48 changes: 48 additions & 0 deletions src/Nodes/FileNode.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
<?php

namespace Square\Hyrule\Nodes;

use Square\Hyrule\Rules\Dimensions;
use Square\Hyrule\Rules\MIMEType;

class FileNode extends AbstractNode
{
protected ?Dimensions $dimensions;

protected ?MIMEType $mimeType;

/**
* @return Dimensions
*/
public function dimensions(): Dimensions
{
if (!isset($this->dimensions)) {
// Create a new Dimensions rule object, push it to the rules array, and
// keep a reference so we can modify it when we need to in the future.
$this->rule($this->dimensions = new Dimensions($this));
}
return $this->dimensions;
}

/**
* @return $this
*/
public function image(): FileNode
{
$this->rule('image');
return $this;
}

/**
* @return MIMEType
*/
public function mimeType(): MIMEType
{
if (!isset($this->mimeType)) {
// Create a new MIMEType rule object, push it to the rules array, and
// keep a reference so we can modify it when we need to in the future.
$this->rule($this->mimeType = new MIMEType($this));
}
return $this->mimeType;
}
}
21 changes: 21 additions & 0 deletions src/Nodes/ObjectNode.php
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,15 @@ public function array(string $name): ArrayNode
return $this->registerNode(new ArrayNode($name, $this));
}

/**
* @param string $name
* @return FileNode
*/
public function file(string $name): FileNode
{
return $this->registerNode(new FileNode($name, $this));
}

/**
* @param string $name
* @return ObjectNode
Expand Down Expand Up @@ -195,6 +204,18 @@ public function arrayWith(string $name, callable $callable): self
return $this;
}

/**
* @param string $name
* @param callable $callable
* @return $this
*/
public function fileWith(string $name, callable $callable): self
{
$this->registerNode(new FileNode($name, $this))
->with($callable);
return $this;
}

/**
* @param string $name
* @param callable $callable
Expand Down
3 changes: 3 additions & 0 deletions src/PHPStan/ArrayNodeEachDynamicReturnExtension.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
use PHPStan\Type\Type;
use Square\Hyrule\Nodes\ArrayNode;
use Square\Hyrule\Nodes\BooleanNode;
use Square\Hyrule\Nodes\FileNode;
use Square\Hyrule\Nodes\FloatNode;
use Square\Hyrule\Nodes\IntegerNode;
use Square\Hyrule\Nodes\NumericNode;
Expand Down Expand Up @@ -65,6 +66,8 @@ public function getTypeFromMethodCall(MethodReflection $methodReflection, Method
return new ObjectType(NumericNode::class);
case "boolean":
return new ObjectType(BooleanNode::class);
case "file":
return new ObjectType(FileNode::class);
default:
return null;
}
Expand Down
29 changes: 29 additions & 0 deletions src/Rules/Dimensions.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<?php

namespace Square\Hyrule\Rules;

use Illuminate\Validation\Rules\Dimensions as DimensionsRule;
use Square\Hyrule\Nodes\FileNode;

/**
* Extends the Laravel's built-in Dimensions helper and adds methods to support Hyrule's fluent API.
*/
class Dimensions extends DimensionsRule
{
private FileNode $node;

/**
* @param FileNode $node
* @param array<string,mixed> $constraints
*/
public function __construct(FileNode $node, array $constraints = [])
{
$this->node = $node;
parent::__construct($constraints);
}

public function end(): FileNode
{
return $this->node;
}
}
Loading

0 comments on commit 99995aa

Please sign in to comment.