Skip to content

Commit

Permalink
Merge pull request #5 from SoftwarePunt/feat-validation-attributes
Browse files Browse the repository at this point in the history
Model Validation
  • Loading branch information
roydejong authored Jun 24, 2024
2 parents c176b83 + 6c15f0a commit 60684e2
Show file tree
Hide file tree
Showing 24 changed files with 682 additions and 27 deletions.
2 changes: 1 addition & 1 deletion .idea/dataSources.local.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

110 changes: 105 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,17 +1,18 @@
# Instarecord
**✨ A nice PHP ORM for MySQL.**
**✨ A hyper productive ORM for PHP.**

[![PHPUnit](https://github.com/SoftwarePunt/instarecord/actions/workflows/phpunit.yml/badge.svg)](https://github.com/SoftwarePunt/instarecord/actions/workflows/phpunit.yml)

*This library is currently used for some internal projects. Extended docs coming soon. Not recommended for production usage yet.*
Instarecord makes it super easy and fun to work with MySQL databases in PHP. It's fast and intuitive, and loaded with optional features to make your life easier.

## The pitch
🧙‍♂️ You define your models with typed variables, and Instarecord figures out the rest, like magic.
# The pitch
🧙‍♂️ Define your models with typed variables, and Instarecord figures out the rest!

```php
<?php

class User extends Model {
class User extends Model
{
public int $id;
public string $email;
public ?string $name;
Expand All @@ -23,3 +24,102 @@ $user->save();

echo "Created user #{$user->id}!";
```

# Features

## 🤝 [Relationships](./docs/Relationships.md)
Define relationships between your models and easily load them in an optimized way.

## [Validation](./docs/Validation.md)
Add constraints to your model properties and validate them with user-friendly error messages.

# Quickstart

## Installation
Add Instarecord to your project with [Composer](https://getcomposer.org/):

```bash
composer require softwarepunt/instarecord
```

## Configuration
Pass your own `DatabaseConfig` or modify the default one:

```php
<?php

use SoftwarePunt\Instarecord\Instarecord;

$config = Instarecord::config();
$config->charset = "utf8mb4";
$config->unix_socket = "/var/run/mysqld/mysqld.sock";
$config->username = "my_user";
$config->password = "my_password";
$config->database = "my_database";
$config->timezone = "UTC";
```

## Use models
Defines your models by creating normal classes with public properties, and extending `Model`:

```php
<?php

use SoftwarePunt\Instarecord\Model;

class Car extends Model
{
public int $id;
public string $make;
public string $model;
public int $year;
}
```

Now you can create, read, update, and delete records with ease:

```php
<?php

$car = new Car();
$car->make = "Toyota";
$car->model = "Corolla";
$car->year = 2005;
$car->save(); // INSERT INTO cars [..]

// Post insert, the primary key (id) is automatically populated

$car->year = 2006;
$car->save(); // UPDATE cars SET year = 2006 WHERE id = 123

$car->delete(); // DELETE FROM cars WHERE id = 123
```

## Run queries
You can easily build and run custom queries, and get results in various ways - from raw data to fully populated models.

### From models
```php
<?php

$matchingCars = Car::query()
->where('make = ?', 'Toyota')
->andWhere('year > ?', 2000)
->orderBy('year', 'DESC')
->limit(10)
->queryAllModels(); // Car[]
```

### Standalone

```php
<?php

use SoftwarePunt\Instarecord\Database;use SoftwarePunt\Instarecord\Instarecord;

$carsPerYear = Instarecord::query()
->select('year, COUNT(*) as count')
->from('cars')
->groupBy('year')
->queryKeyValueArray(); // [2005 => 10, 2006 => 5, ..]
```
87 changes: 87 additions & 0 deletions docs/Validation.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
# Validation
**Instarecord offers optional, built-in validation of model instances.**

## Introduction

### Setting constraints
You can specify constraints by adding attributes to the model properties.

```php
<?php

use Instarecord\Model;
use SoftwarePunt\Instarecord\Attributes\FriendlyName;use SoftwarePunt\Instarecord\Attributes\MaxLength;
use SoftwarePunt\Instarecord\Attributes\MinLength;

class User extends Model
{
#[Required]
#[MinLength(3, "Your name is too short!")]
public string $name;

#[Required]
#[MaxLength(255)]
#[FriendlyName("E-mail address")]
public string $email;
}
```

### Running validations
⚠️ Validations are currently not run automatically when saving a model. You must call `validate()` manually.

You can validate a model by simply calling `validate()` on it:

```php
$user = new User();
$user->name = "A";
$user->email = "";
$result = $user->validate();

if (!$result->ok) {
foreach ($result->errors as $error) {
echo $error->message . "\n";
}
}
```

The example above will output:

```
Your name is too short!
E-mail address is required.
```

## Validation attributes

Validation attributes live in the `Instarecord\Attributes` namespace.

### `#[FriendlyName(string $friendlyName)]`

Specifies a friendly name for the property. This is used in default error messages.

If no friendly name is specified, the property name is used.

### `#[Required]`

The property is required and its value cannot be:

- `null`
- empty string
- whitespace-only string
- `false`
- zero
- empty array

> {name} is required.
### `#[MinLength(int $minLength, ?string $customError = null)]`

Requires the property to be non-null, non-empty and at least `$minLength` characters long.

> {name} must be at least {minLength} characters.
### `#[MaxLength(int $maxLength, ?string $customError = null)]`

Requires the property to be at most `$maxLength` characters long.

> {name} can't be longer than {maxLength} characters.
16 changes: 16 additions & 0 deletions lib/Attributes/FriendlyName.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<?php

namespace SoftwarePunt\Instarecord\Attributes;

use Attribute;

/**
* Specifies a user-friendly name for a property, used in validation error messages.
*/
#[Attribute(Attribute::TARGET_PROPERTY)]
class FriendlyName
{
public function __construct(public string $name)
{
}
}
35 changes: 35 additions & 0 deletions lib/Attributes/MaxLength.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
<?php

namespace SoftwarePunt\Instarecord\Attributes;

use Attribute;
use SoftwarePunt\Instarecord\Validation\ValidationAttribute;
use SoftwarePunt\Instarecord\Validation\ValidationResult;

/**
* Specifies the maximum length of a model property value.
*
* WIP: When used with migrations, this represents the maximum length of the column.
*/
#[Attribute(Attribute::TARGET_PROPERTY)]
class MaxLength extends ValidationAttribute
{
public function __construct(public int $maxLength, public ?string $customError = null)
{
}

public function validate(string $name, mixed $value): ValidationResult
{
if (empty($value))
// Do not validate null/empty values
return ValidationResult::pass();

if (strlen($value) <= $this->maxLength)
// Pass: The value is shorter than/equal to the maximum length
return ValidationResult::pass();

return ValidationResult::fail(
$this->customError ?? "{$name} can't be longer than {$this->maxLength} characters"
);
}
}
29 changes: 29 additions & 0 deletions lib/Attributes/MinLength.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<?php

namespace SoftwarePunt\Instarecord\Attributes;

use Attribute;
use SoftwarePunt\Instarecord\Validation\ValidationAttribute;
use SoftwarePunt\Instarecord\Validation\ValidationResult;

/**
* Specifies the minimum length of a model property value.
*/
#[Attribute(Attribute::TARGET_PROPERTY)]
class MinLength extends ValidationAttribute
{
public function __construct(public int $minLength, public ?string $customError = null)
{
}

public function validate(string $name, mixed $value): ValidationResult
{
if (!empty($value) && strlen($value) >= $this->minLength)
// Pass: The value is not null/empty and is at least the minimum length
return ValidationResult::pass();

return ValidationResult::fail(
$this->customError ?? "{$name} must be at least {$this->minLength} characters"
);
}
}
27 changes: 27 additions & 0 deletions lib/Attributes/Required.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<?php

namespace SoftwarePunt\Instarecord\Attributes;

use Attribute;
use SoftwarePunt\Instarecord\Validation\ValidationAttribute;
use SoftwarePunt\Instarecord\Validation\ValidationResult;

/**
* Marks a model property as required (not null, empty or whitespace).
*/
#[Attribute(Attribute::TARGET_PROPERTY)]
class Required extends ValidationAttribute
{
public function __construct(public ?string $customError = null)
{
}

public function validate(string $name, mixed $value): ValidationResult
{
if ($value === null || $value === "" || (is_string($value) && trim($value) === "")
|| $value === false || $value === 0 || (is_array($value) && empty($value)))
return ValidationResult::fail($this->customError ?? "{$name} is required");

return ValidationResult::pass();
}
}
16 changes: 16 additions & 0 deletions lib/Attributes/TableName.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<?php

namespace SoftwarePunt\Instarecord\Attributes;

use Attribute;

/**
* Specifies a custom name for a model's backing table.
*/
#[Attribute(Attribute::TARGET_CLASS)]
class TableName
{
public function __construct(public string $name)
{
}
}
Loading

0 comments on commit 60684e2

Please sign in to comment.