OneBot: Leantime 改造版源码(BOM/Univer 表格/AI 接管/品牌替换等)
This commit is contained in:
108
app/Core/Domains/BaseService.php
Normal file
108
app/Core/Domains/BaseService.php
Normal file
@@ -0,0 +1,108 @@
|
||||
<?php
|
||||
|
||||
namespace Leantime\Core\Domains;
|
||||
|
||||
use Leantime\Core\Auth\Permissions\PermissionService;
|
||||
use Leantime\Core\Events\DispatchesEvents;
|
||||
use Leantime\Core\Exceptions\ValidationException;
|
||||
|
||||
/**
|
||||
* Base class for domain services, providing the cross-cutting authorization and validation
|
||||
* helpers a service needs. All services should extend this (it also carries the
|
||||
* {@see DomainService} marker and the {@see DispatchesEvents} trait, so event behavior is
|
||||
* unchanged).
|
||||
*
|
||||
* Dependency wiring is handled by {@see \Leantime\Core\Auth\Permissions\PermissionServiceProvider}:
|
||||
* an `afterResolving(BaseService::class, ...)` hook wires a LAZY resolver (not the instance) on
|
||||
* every container-resolved subclass. That keeps the engine injected with zero constructor
|
||||
* boilerplate in subclasses (which all have their own repo-injecting constructors) and without
|
||||
* reaching for the `app()` helper inside service methods. The resolver — rather than eager
|
||||
* injection — is essential: a service can sit inside PermissionService's own dependency graph
|
||||
* (the Files service is reached via PermissionService → ChecksProjectAccess → Projects → Files),
|
||||
* so eagerly making PermissionService inside that service's afterResolving hook would re-enter
|
||||
* PermissionService's half-built construction and recurse forever. Resolving lazily on first
|
||||
* authorize()/can() defers it until the singleton exists.
|
||||
*/
|
||||
abstract class BaseService implements DomainService
|
||||
{
|
||||
use DispatchesEvents;
|
||||
|
||||
protected ?PermissionService $permissions = null;
|
||||
|
||||
/** @var (\Closure(): PermissionService)|null Lazy resolver wired by PermissionServiceProvider. */
|
||||
protected ?\Closure $permissionServiceResolver = null;
|
||||
|
||||
/**
|
||||
* Set the engine instance directly. Used by unit tests; production uses the lazy resolver below.
|
||||
*/
|
||||
public function setPermissionService(PermissionService $permissions): void
|
||||
{
|
||||
$this->permissions = $permissions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Wire a LAZY resolver instead of the instance (see the class docblock for why eager injection
|
||||
* recurses). The engine is resolved on first authorize()/can().
|
||||
*/
|
||||
public function setPermissionServiceResolver(\Closure $resolver): void
|
||||
{
|
||||
$this->permissionServiceResolver = $resolver;
|
||||
}
|
||||
|
||||
/** Resolve the engine, preferring a directly-set instance (tests) then the lazy resolver. */
|
||||
private function permissionService(): PermissionService
|
||||
{
|
||||
if ($this->permissions === null) {
|
||||
if ($this->permissionServiceResolver === null) {
|
||||
throw new \LogicException(static::class.' has no PermissionService: it was neither resolved through the container nor wired in a test.');
|
||||
}
|
||||
|
||||
$this->permissions = ($this->permissionServiceResolver)();
|
||||
}
|
||||
|
||||
return $this->permissions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Authorize the current user for a `domain.action` permission or throw. Replaces the
|
||||
* silent `return false` pattern — a denial becomes an
|
||||
* {@see \Leantime\Core\Exceptions\AuthorizationException} (403 web / RPC -32001).
|
||||
*
|
||||
* @throws \Leantime\Core\Exceptions\AuthorizationException
|
||||
*/
|
||||
protected function authorize(string $permission, ?int $projectId = null, ?bool $forceGlobal = null): void
|
||||
{
|
||||
$this->permissionService()->authorize($permission, $projectId, $forceGlobal);
|
||||
}
|
||||
|
||||
/** Non-throwing capability check, for branching. */
|
||||
protected function can(string $permission, ?int $projectId = null, ?bool $forceGlobal = null): bool
|
||||
{
|
||||
return $this->permissionService()->currentUserCan($permission, $projectId, $forceGlobal);
|
||||
}
|
||||
|
||||
/** The authenticated user's id, or null when there is no session user. */
|
||||
protected function currentUserId(): ?int
|
||||
{
|
||||
$id = session('userdata.id');
|
||||
|
||||
return ($id === null || $id === '') ? null : (int) $id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate input against Laravel rules, returning the validated (whitelisted) subset or
|
||||
* throwing a {@see ValidationException} (422 web / RPC -32602 with field errors). Works
|
||||
* identically whether input arrived via a controller or JSON-RPC.
|
||||
*
|
||||
* @param array<string, mixed> $data
|
||||
* @param array<string, mixed> $rules
|
||||
* @param array<string, string> $messages
|
||||
* @return array<string, mixed>
|
||||
*
|
||||
* @throws ValidationException
|
||||
*/
|
||||
protected function validate(array $data, array $rules, array $messages = []): array
|
||||
{
|
||||
return ValidationException::validate($data, $rules, $messages);
|
||||
}
|
||||
}
|
||||
85
app/Core/Domains/DTO.php
Normal file
85
app/Core/Domains/DTO.php
Normal file
@@ -0,0 +1,85 @@
|
||||
<?php
|
||||
|
||||
namespace Leantime\Core\Domains;
|
||||
|
||||
use Illuminate\Http\Client\Response;
|
||||
use Illuminate\Support\Arr;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
abstract class DTO
|
||||
{
|
||||
/**
|
||||
* @param Response|array $data The data to map to the DTO
|
||||
**/
|
||||
public function __construct(private Response|array $data)
|
||||
{
|
||||
$data = Arr::dot($data instanceof Response ? $data->json() : $data);
|
||||
$builder = build($this);
|
||||
$propertyAttributes = collect((new \ReflectionClass($this))->getProperties())->mapWithKeys(function ($property) {
|
||||
$property->setAccessible(true);
|
||||
|
||||
return [$property->getName() => collect($property->getAttributes())->mapWithKeys(fn ($attr) => [$attr->getName() => $attr->getArguments()])];
|
||||
})->all();
|
||||
$propertyAttributes = Arr::dot($propertyAttributes);
|
||||
|
||||
foreach ($data as $placement => $value) {
|
||||
$propertyPath = explode('.', Str::beforeLast($placement, '.'));
|
||||
$propertyName = array_shift($propertyPath);
|
||||
|
||||
if (
|
||||
($propKey = array_search($placement, $propertyAttributes))
|
||||
&& Str::afterLast($propKey, '.') == 'Map'
|
||||
) {
|
||||
$propertyPath = explode('.', Str::beforeLast($propKey, '.'));
|
||||
$propertyName = array_shift($propertyPath);
|
||||
}
|
||||
|
||||
$placement = implode('.', array_filter([$propertyName, ...$propertyPath]));
|
||||
$attributes = array_filter(
|
||||
$propertyAttributes,
|
||||
fn ($key) => Str::beforeLast('.', $key) == $placement && Str::afterLast('.', $key) !== 'Map',
|
||||
ARRAY_FILTER_USE_KEY
|
||||
);
|
||||
|
||||
foreach ($attributes as $key => $attrValue) {
|
||||
$attrName = Str::afterLast($key, '.');
|
||||
$value = $this->{Str::camel($attrName)}(params: $attrValue, value: $value);
|
||||
}
|
||||
|
||||
$builder->set($placement, $value);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates values.
|
||||
*
|
||||
* @param string[] $params validations rules to apply to the value
|
||||
*
|
||||
* @todo Implement. May use illuminate/validation later on.
|
||||
*
|
||||
* @see https://github.com/mattstauffer/Torch/tree/master/components/validation
|
||||
**/
|
||||
private function validate(array $params, mixed $value): mixed
|
||||
{
|
||||
return $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the DTO data as multidimensional an array
|
||||
**/
|
||||
public function toArray(): array
|
||||
{
|
||||
$props = get_class_vars($this::class);
|
||||
unset($props['data']);
|
||||
|
||||
return collect($props)->map(fn ($defaultVal, $key) => $this->{$key} ?? $defaultVal)->all();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the DTO data as multidimensional an array
|
||||
**/
|
||||
public function all(): array
|
||||
{
|
||||
return $this->toArray();
|
||||
}
|
||||
}
|
||||
5
app/Core/Domains/DomainModel.php
Normal file
5
app/Core/Domains/DomainModel.php
Normal file
@@ -0,0 +1,5 @@
|
||||
<?php
|
||||
|
||||
namespace Leantime\Core\Domains;
|
||||
|
||||
interface DomainModel {}
|
||||
58
app/Core/Domains/DomainRepository.php
Normal file
58
app/Core/Domains/DomainRepository.php
Normal file
@@ -0,0 +1,58 @@
|
||||
<?php
|
||||
|
||||
namespace Leantime\Core\Domains;
|
||||
|
||||
/**
|
||||
* Service Interface - Base interface for all services
|
||||
*/
|
||||
interface DomainRepository
|
||||
{
|
||||
/**
|
||||
* patches the object by key.
|
||||
*
|
||||
* @param int $id Id of the object to be patched
|
||||
* @param array $params Key=>value array where key represents the object field name and value the value.
|
||||
* @return bool returns true on success, false on failure
|
||||
*/
|
||||
public function patch(int $id, array $params): bool;
|
||||
|
||||
/**
|
||||
* updates the object by key.
|
||||
*
|
||||
* @param object|array $object expects the entire object to be updated as object or array
|
||||
* @return array|bool Returns true on success, false on failure
|
||||
*/
|
||||
public function update(object|array $object): array|bool;
|
||||
|
||||
/**
|
||||
* Creates a new object
|
||||
*
|
||||
* @param object|array $object Object or array to be created
|
||||
* @return int|false Returns id of new element or false
|
||||
*/
|
||||
public function create(object|array $object): int|false;
|
||||
|
||||
/**
|
||||
* Deletes object
|
||||
*
|
||||
* @param int $id Id of the object to be deleted
|
||||
* @return bool Returns id of new element or false
|
||||
*/
|
||||
public function delete(int $id);
|
||||
|
||||
/**
|
||||
* Gets 1 specific item
|
||||
*
|
||||
* @param int $id Id of the object to be retrieved
|
||||
* @return object|array|false Returns object or array. False on failure or if item cannot be found
|
||||
*/
|
||||
public function get(int $id);
|
||||
|
||||
/**
|
||||
* Get all items
|
||||
*
|
||||
* @param array|null $searchparams Search parameters
|
||||
* @return array|false Returns array on success, false on failure. No results should return empty array
|
||||
*/
|
||||
public function query(?array $searchparams = null);
|
||||
}
|
||||
18
app/Core/Domains/DomainService.php
Normal file
18
app/Core/Domains/DomainService.php
Normal file
@@ -0,0 +1,18 @@
|
||||
<?php
|
||||
|
||||
namespace Leantime\Core\Domains;
|
||||
|
||||
/**
|
||||
* Marker interface for all domain (and plugin) service classes.
|
||||
*
|
||||
* It carries no required methods on purpose. Real services have wildly different shapes
|
||||
* (the Tickets service alone has ~75 heterogeneous methods), so a fixed CRUD contract
|
||||
* never fit — which is exactly why the previous patch/update/create/delete/get/query
|
||||
* interface had zero implementers. This marker instead gives the service layer a single
|
||||
* type to scan for and a shared home (via {@see BaseService}) for the cross-cutting
|
||||
* authorize()/validate() helpers, without forcing a fictional method surface.
|
||||
*
|
||||
* Granular capability interfaces (e.g. a real Crudable) may be introduced alongside this
|
||||
* marker where they genuinely apply, opt-in per service.
|
||||
*/
|
||||
interface DomainService {}
|
||||
Reference in New Issue
Block a user