Skip to content

Commit

Permalink
readmes and license
Browse files Browse the repository at this point in the history
  • Loading branch information
jakubkulhan committed May 24, 2024
1 parent 7d52be0 commit 96c1482
Show file tree
Hide file tree
Showing 9 changed files with 355 additions and 1 deletion.
22 changes: 22 additions & 0 deletions LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
Copyright (c) 2024 Jakub Kulhan <ahoj@jakubkulhan.cz>

Permission is hereby granted, free of charge, to any person
obtaining a copy of this software and associated documentation
files (the "Software"), to deal in the Software without
restriction, including without limitation the rights to use,
copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the
Software is furnished to do so, subject to the following
conditions:

The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
OTHER DEALINGS IN THE SOFTWARE.
22 changes: 22 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# DataAccessKit (source repository)

> Type-safe minimum-writing SQL repositories for PHP
## Packages

- [DataAccessKit](https://github.com/jakubkulhan/data-access-kit#readme) - Persistence layer based on Doctrine\DBAL and repository generator.
- [DataAccessKit\Symfony](https://github.com/jakubkulhan/data-access-kit-symfony#readme) - Integration with Symfony framework.

## Contributing

This is the main source repository. Other repositories are automatically split from this one. Please open issues and pull requests here.

### Testing

```shell
composer test
```

## License

Licensed under MIT license. See [LICENSE](LICENSE).
3 changes: 3 additions & 0 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,5 +26,8 @@
"DataAccessKit\\": "data-access-kit/test/",
"DataAccessKit\\Symfony\\": "data-access-kit-symfony/test/"
}
},
"scripts": {
"test": "phpunit"
}
}
48 changes: 48 additions & 0 deletions data-access-kit-symfony/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
# DataAccessKit\Symfony

## Quick start

Add bundle to `config/bundles.php`.

```php
<?php

return [
// ...
Doctrine\Bundle\DoctrineBundle\DoctrineBundle::class => ['all' => true], // DataAccessKit depends on Doctrine\DBAL
DataAccessKit\Symfony\DataAccessKitBundle::class => ['all' => true],
];
```

(Or add to `Kernel::registerBundles()` if you don't use `MicroKernelTrait`.)

Then configure paths to your repository classes in `config/packages/data_access_kit.yaml`.

```yaml
data_access_kit:
paths:
# the code must be structured in PSR-4 way
- path: %kernel.project_dir%/src/Repository
namespace: App\Repository
```
And that's it! Follow repositories [quick start](https://github.com/jakubkulhan/data-access-kit#quick-start) to learn more.
## Installation
```shell
composer require data-access-kit/data-access-kit-symfony@dev-main
```

### Requirements

- PHP 8.3 or higher.
- Symfony 7.0 or higher.

## Contributing

This repository is automatically split from the [main repository](https://github.com/jakubkulhan/data-access-kit-src). Please open issues and pull requests there.

## License

Licensed under MIT license. See [LICENSE](https://github.com/jakubkulhan/data-access-kit-src/blob/main/LICENSE).
225 changes: 225 additions & 0 deletions data-access-kit/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,225 @@
# DataAccessKit

> Type-safe minimum-writing SQL repositories for PHP
## Quick start

Start by creating an object.

```php
use DataAccessKit\Attribute\Table;
use DataAccessKit\Attribute\Column;

#[Table]
class User
{
public function __construct(
#[Column]
public int $id,
#[Column]
public string $name,
)
{
}
}
```

Then create a repository interface.

```php
use DataAccessKit\Repository\Attribute\Repository;

#[Repository(User::class)]
interface UserRepositoryInterface
{
public function getById(int $id): User;
}
```

By an integration with your framework (e.g. [Symfony](https://github.com/jakubkulhan/data-access-kit-symfony#readme)), a repository implementation will be generated for you and you can use it in your services.

```php
class UserService
{
public function __construct(
private UserRepositoryInterface $userRepository,
)
{
}

public function login(int $userId): void
{
$user = $this->userRepository->getById($userId);

// ...
}
}
```

## Installation

```shell
composer require data-access-kit/data-access-kit@dev-main
```

### Requirements

- PHP 8.3 or higher.

## Object attributes

DataAccessKit maps plain old PHP objects to database using [attributes](https://www.php.net/manual/en/language.attributes.overview.php).

```php
#[Table(
name: "users",
)]
```

[Table](./src/Attribute/Table.php) attribute connects class to a database table.

- `name` specifies the table name. If not provided, the table name is derived from the class name by `NameConverterInterface`. Default name converter converts CamelCase to snake_case and pluralizes the name (i.e. `User` to `users`, `UserCredential` to `user_credentials`).

```php
#[Column(
name: "user_id",
primary: true,
generated: true,
)]
```

[Column](./src/Attribute/Column.php) attribute is supposed to be added above class property.

- `name` argument, same as with Table, is optional and if omitted the column name is derived from the property name by `NameConverterInterface`. Default name converter converts CamelCase to snake_case (i.e. `userId` to `user_id`). The `primary` argument specifies whether the column is a primary key.
- `primary` means that the column is a part of the primary key. When UPDATE or DELETE is called, values from properties annotated with `#[Column(primary: true)]` are used in WHERE clause. When INSERT is called, values from properties annotated with `#[Column(primary: true)]` are not used in the query (they are not part of the INSERT statement), but if you INSERT only one row with a single primary property, it is populated from `LAST_INSERT_ID()` (or equivalent) after the query.
- `generated` specifies whether the column is generated by the database (e.g. auto increment, but also for [generated columns](https://dev.mysql.com/doc/refman/8.0/en/create-table-generated-columns.html)). Generated columns are only read from the database (they figure in SELECTs), but not written to it (they are not used in INSERTs, UPDATEs).

## Persistence

Persistence layer is based on [Doctrine\DBAL](https://www.doctrine-project.org/projects/dbal.html). It is a thin layer on top of it, providing type-safe object mapping from and to database based on object attributes. See [PersistenceInterface](./src/PersistenceInterface.php) for more details.

## Repositories

Repositories are generated from interfaces. The interface needs to be annotated with [Repository](./src/Repository/Attribute/Repository.php) attribute.

```php
#[Repository(
class: User::class,
)]
```

- `class` specifies the class of the entity the repository is for.

Interface methods are then implemented based on what attribute they are annotated with. If a method doesn't have any of the method attributes, the compiler tries to determine the method attribute based on the method name. Methods starting with `find` and `get` are considered as Find methods, methods starting with `count` are considered as Count methods. If a method attribute cannot be determined, the compiler throws an exception.

### Find

Find methods are used to retrieve entities from the database.

They can return a single entity or a collection of entities. Supported return types for collections are `iterable` and `array`.

A single entity return type must be the class the repository is for. If the return type is non-nullable and no rows is returned from the database, the method throws an exception. Also, if multiple rows are returned from the database, an exception is thrown.

```php
#[Find(
select: "%columns(alias: u, except: password)", // optional, default is all columns specified by Column attributes
from: "users", // optional, default is the table the repository is for
alias: "u", // optional, default is "t"
where: "u.id = @id", // optional, default
orderBy: "u.name", // optional, default is empty
limit: 1, // optional, default is no limit
offset: 10, // optional, default is no offset
)]
public function find(int $id): User;
```

- `select` - the columns to select. [Macros and variables](#macros-and-variables) available.
- `from` - the table to select from.
- `alias` - the table alias.
- `where` - the WHERE clause. [Macros and variables](#macros-and-variables) available.
- `orderBy` - the ORDER BY clause. [Macros and variables](#macros-and-variables) available.
- `limit` - the LIMIT clause.
- `offset` - the OFFSET clause.

### Count

Count methods return number of rows in the database. They must return `int`.

```php
#[Count(
from: "users", // optional, default is the table the repository is for
alias: "u", // optional, default is "t"
where: "u.id = @id" // optional, default is AND of columns specified by the method parameters
)]
public function count(int $id): int;
```

- `from` - the table to count from.
- `alias` - the table alias.
- `where` - the WHERE clause. [Macros and variables](#macros-and-variables) available.

### SQL

SQL methods execute arbitrary SQL queries. They can return a single entity, a collection of entities, or a scalar value.

```php
#[SQL(
sql: "SELECT * FROM users u WHERE u.name = @name", // required
itemType: User::class, // optional
)]
public function sql(string $name): iterable;
```

- `sql` - the SQL query.
- `itemType` - the type of the item if the query returns an `iterable` or `array` and the item type is different from the class the repository is for. The use case is e.g. when you want to retrieve custom aggregation of the data and map it to objects.

#### Macros and variables

To reference a method argument in the SQL query, you can use `@` followed by the parameter name (e.g. `@id`). This is then replaced by a placeholder in the actual SQL query and bound to the argument in the statement.

Array parameters are expanded to a list of placeholders. For example, if you have a method with an array parameter `ids`, you can use `@ids` in the SQL query, and it will be expanded to `?, ?, ?, ...` and bound to the values from array.

There are also several macros that expand to SQL fragments.

- `%columns` - expands to all columns specified by Column attributes.
- `%columns(alias: u)` - expands to all columns specified by Column attributes prefixed by the alias.
- `%columns(except: password)` - expands to all columns specified by Column attributes except the specified columns.
- `%columns(alias: u, except: password)` - combination of the previous two.
- `%table` - expands to the table name.

### SQLFile

The same as SQL attribute, but the SQL query is loaded from a file.

```php
#[SQLFile(
file: "sql/find_by_name.sql", // required
itemType: User::class, // optional
)]
public function sqlFile(string $name): iterable;
```

### Delegate

If a repository method is more complex than what can be expressed by a single SQL query, you will probably want to implement it yourself.

```php
#[Delegate(
class: UserRepositoryDelegate::class, // required
method: "delegateMethodName", // optional, default is the same name as the annotated method
)]
public function methodName(): array;
```

- `class` - the class that implements the method. It can be a class, an interface, or a trait.
- Classes and interfaces are then added as a constructor parameter in the generated repository class.
- Traits are used by an anonymous class instantiated in the generated repository's constructor. If the trait has a constructor, its parameters are added as constructor parameters in the generated repository class.
- `method` - target method in the class. If not provided, the interface method name is used.

## Contributing

This repository is automatically split from the [main repository](https://github.com/jakubkulhan/data-access-kit-src). Please open issues and pull requests there.

## License

Licensed under MIT license. See [LICENSE](https://github.com/jakubkulhan/data-access-kit-src/blob/main/LICENSE).
2 changes: 1 addition & 1 deletion data-access-kit/src/Persistence.php
Original file line number Diff line number Diff line change
Expand Up @@ -167,7 +167,7 @@ public function update(object $object, ?array $columns = null): void
if ($column->primary) {
$where[] = $platform->quoteSingleIdentifier($column->name) . " = ?";
$whereValues[] = $value;
} else if ($columns === null || in_array($column->name, $columns, true)) {
} else if (!$column->generated && ($columns === null || in_array($column->name, $columns, true))) {
$set[] = $platform->quoteSingleIdentifier($column->name) . " = ?";
$setValues[] = $value;
}
Expand Down
7 changes: 7 additions & 0 deletions data-access-kit/src/Repository/Attribute/Count.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,11 @@
#[Attribute(Attribute::TARGET_METHOD)]
class Count
{
public function __construct(
public readonly ?string $from = null,
public readonly string $alias = "t",
public readonly ?string $where = null,
)
{
}
}
11 changes: 11 additions & 0 deletions data-access-kit/src/Repository/Attribute/Find.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,15 @@
#[Attribute(Attribute::TARGET_METHOD)]
class Find
{
public function __construct(
public readonly ?string $select = null,
public readonly ?string $from = null,
public readonly string $alias = "t",
public readonly ?string $where = null,
public readonly ?string $orderBy = null,
public readonly ?string $limit = null,
public readonly ?string $offset = null,
)
{
}
}
16 changes: 16 additions & 0 deletions data-access-kit/src/Repository/Attribute/SQLFile.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<?php declare(strict_types=1);

namespace DataAccessKit\Repository\Attribute;

use Attribute;

#[Attribute(Attribute::TARGET_METHOD)]
class SQLFile
{
public function __construct(
public readonly string $file,
public readonly ?string $itemType = null,
)
{
}
}

0 comments on commit 96c1482

Please sign in to comment.