Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ on:
jobs:
build:
if: github.event.pull_request.merged == true
runs-on: ubuntu-latest
runs-on: [self-hosted, debian-13-sh]
steps:
- uses: actions/checkout@v4
- name: Build install archive
Expand Down
11 changes: 9 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,15 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]
## 1.0.1

### Added

- Forgejo → GitHub publish pipeline, Packagist notify workflows, and library-oriented issue templates
- `AbstractWith` extends `\Lombok\Helper` and forwards unmatched `__call` to Lombok after `WithHandler`
- Runtime dependency on `marcin-orlowski/lombok-php` `^1.2` for `#[Getter]` / `#[Setter]`

## 1.0.0

### Added

- Initial release
18 changes: 13 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

Reusable PHP 8.3 attributes and runtime helpers.

See [docs/getting-started.md](docs/getting-started.md) for full documentation.
See [the documentation](https://readme.devcraft.club/dev/dev-tools/1.0.1/getting_started) for the full guide.

## Local installation

Expand All @@ -18,7 +18,7 @@ Add a Composer path repository in the consuming project and require the package:
}
],
"require": {
"devcraft/dev-tools": "@dev"
"devcraftclub/dev-tools": "@dev"
}
}
```
Expand All @@ -27,13 +27,17 @@ Path repositories must be configured by the consuming root project; Composer
does not inherit them transitively. Publish this package later, or replace the
path repository with a normal repository and version.

The package depends on [`marcin-orlowski/lombok-php`](https://github.com/MarcinOrlowski/lombok-php) `^1.2`.

## Fluent properties

```php
use Lombok\Getter;
use Devcraft\Abstracts\AbstractWith;
use Devcraft\Attributes\With;
use Devcraft\Attributes\WithItem;

#[Getter]
final class Query extends AbstractWith
{
#[With]
Expand All @@ -47,6 +51,10 @@ final class Query extends AbstractWith
}
```

`AbstractWith` routes unresolved calls through `WithHandler::handles()` and
`WithHandler::call()`. One `WithItem` descriptor appends a value. Two
descriptors set a key/value pair. Arrays in a descriptor represent union types.
`AbstractWith` extends `\Lombok\Helper`. Unresolved calls go to `WithHandler`
first (`with*` / `with*Item`), then to Lombok getters and setters (`get*` /
`set*` / `is*`). One `WithItem` descriptor appends a value. Two descriptors set
a key/value pair. Arrays in a descriptor represent union types.

If a subclass defines `__construct()`, call `parent::__construct()` so Lombok
can wire accessors. `with*` methods do not depend on that call.
10 changes: 5 additions & 5 deletions composer.json
Original file line number Diff line number Diff line change
@@ -1,19 +1,19 @@
{
"name": "devcraftclub/dev-tools",
"description": "Reusable PHP attributes and validation utilities",
"type": "library",
"version": "1.0.0",
"version": "1.0.1",
"homepage": "https://github.com/DevCraftClub/DevTools",
"support": {
"email": "dev@devcraft.club",
"issues": "https://github.com/DevCraftClub/DevTools/issues",
"forum": "https://devcraft.club",
"docs": "https://readme.devcraft.club/dev/dev-tools/1.0.0/guides/english"
"docs": "https://readme.devcraft.club/dev/dev-tools/1.0.1/en/getting_started"
},
"require": {
"php": ">=8.3",
"psr/log": "^3.0",
"analog/analog": "^1.0"
"analog/analog": "^1.0",
"marcin-orlowski/lombok-php": "^1.2"
},
"require-dev": {
"phpunit/phpunit": "^11.0"
Expand Down Expand Up @@ -43,7 +43,7 @@
"name": "Maxim Harder",
"email": "dev@devcraft.club",
"role": "developer",
"homepage": "https://devcraft.club"
"homepage": "https://maxim-harder.de"
},
{
"name": "DevCraft",
Expand Down
2 changes: 1 addition & 1 deletion manifest.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
{
"version": "1.0.0",
"version": "1.0.1",
"name": "dev-tools"
}
10 changes: 3 additions & 7 deletions src/Abstracts/AbstractWith.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,21 +4,17 @@

namespace Devcraft\Abstracts;

use BadMethodCallException;
use Lombok\Helper;
use Devcraft\Runtime\WithHandler;

abstract class AbstractWith {
abstract class AbstractWith extends Helper {

public function __call(string $methodName, array $arguments): mixed {
if(WithHandler::handles($this, $methodName)) {
return WithHandler::call($this, $methodName, $arguments);
}

throw new BadMethodCallException(sprintf(
'Call to undefined method %s::%s()',
$this::class,
$methodName,
));
return parent::__call($methodName, $arguments);
}

}
89 changes: 45 additions & 44 deletions tests/Abstracts/AbstractWithTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,57 +4,58 @@

namespace Devcraft\DevTools\Tests\Abstracts;

use Lombok\Getter;
use Lombok\Setter;
use BadMethodCallException;
use Devcraft\Abstracts\AbstractWith;
use Devcraft\Attributes\With;
use PHPUnit\Framework\TestCase;
use Devcraft\Attributes\With;
use Devcraft\Abstracts\AbstractWith;

final class AbstractWithFixture extends AbstractWith
#[Getter, Setter]
final class AccessorFixture extends AbstractWith
{
#[With]
private ?int $page = null;
#[With]
private ?int $page = null;

#[With]
private string $cursor = '';

public function page(): ?int
{
return $this->page;
}

public function cursor(): string
{
return $this->cursor;
}
private bool $visible = false;
}

final class AbstractWithTest extends TestCase
{
public function testRoutesKnownVirtualMethodsAndSupportsFluentChaining(): void
{
$fixture = new AbstractWithFixture();

$returned = $fixture
->withPage(7)
->withCursor('next-page');

self::assertSame($fixture, $returned);
self::assertSame(7, $fixture->page());
self::assertSame('next-page', $fixture->cursor());
}

public function testUnknownMethodThrowsBadMethodCallException(): void
{
$fixture = new AbstractWithFixture();

$this->expectException(BadMethodCallException::class);
$this->expectExceptionMessage(sprintf(
'Call to undefined method %s::%s()',
AbstractWithFixture::class,
'missing',
));

$fixture->missing();
}
}
public function testWithRoutesToWithHandlerAndGetterReadsValue(): void
{
$fixture = new AccessorFixture();

$returned = $fixture->withPage(4);

self::assertSame($fixture, $returned);
self::assertSame(4, $fixture->getPage());
}

public function testSetterMutatesAndReturnsSameInstance(): void
{
$fixture = new AccessorFixture();

$returned = $fixture->setPage(9);

self::assertSame($fixture, $returned);
self::assertSame(9, $fixture->getPage());
}

public function testBooleanPropertyUsesIsPrefix(): void
{
$fixture = new AccessorFixture();

self::assertFalse($fixture->isVisible());
self::assertSame($fixture, $fixture->setVisible(true));
self::assertTrue($fixture->isVisible());
}

public function testUnknownMethodThrowsBadMethodCallException(): void
{
$fixture = new AccessorFixture();

$this->expectException(BadMethodCallException::class);
$fixture->missing();
}
}
Loading