Waiting for breaking change improvement proposals until V1 Q4 2026 release #2
Replies: 29 comments 29 replies
|
Contextual bindings will be removed to avoid developer convenience slowing down the execution with no real gains. |
|
Event Subscribers could be next on the removal list: The Not Recommended Way (Event Subscriber): <?php
namespace App\Listeners;
use MacropaySolutions\Kernel\Auth\Events\Login;
use MacropaySolutions\Kernel\Auth\Events\Logout;
use MacropaySolutions\Kernel\Events\Dispatcher;
class UserEventSubscriber
{
public function handleUserLogin(Login $event): void {}
public function handleUserLogout(Logout $event): void {}
public function subscribe(Dispatcher $events): void
{
$events->listen(Login::class, [UserEventSubscriber::class, 'handleUserLogin']);
$events->listen(Logout::class, [UserEventSubscriber::class, 'handleUserLogout']);
}
}
// Register in EventServiceProvider:
protected $subscribe = [
UserEventSubscriber::class, // ❌ NOT RECOMMENDED
];Problem: This subscriber gets instantiated and registered every single request. Framework has to dynamically discover and boot it each time, bypassing the caching layer. The Recommended Way (Class-based Listeners): <?php
namespace App\Listeners;
use MacropaySolutions\Kernel\Auth\Events\Login;
class HandleUserLogin
{
public function handle(Login $event): void {}
}
// Register in EventServiceProvider:
protected $listen = [
Login::class => [
HandleUserLogin::class, // ✅ RECOMMENDED
],
];
// Then cache it:
// php artisan event:cacheBenefit: The event:cache command compiles these listeners into memory once, so they're available instantly on every request without dynamic registration overhead. |
|
Json Resources will go also. As stated in docs, they are replaced by php-crufd-wizard + php-crufd-wizard-decorator The Base Classes: The Response Handlers: The "Magic" Traits & Helpers (The source of the conditional logic bugs): |
|
Architectural Note: High-Performance Timestamps, Soft Deletes & Migrations by Gemini (It contains the high level idea. The code is not acc to PSR12 and it contains the else word... Also $dates should be removed and replaced by casts) Standard Obvious relies on heavy Carbon objects, expensive Database Grammar lookups, and dynamic type-casting just to save, delete, and read dates. Furthermore, standard migrations default to SQL TIMESTAMP columns, which perform hidden, database-level timezone conversions. To achieve maximum performance and perfect timezone predictability, I will modify the Kernel to process timestamps strictly as raw PHP strings natively, and ensure the database stores them as literal strings (DATETIME). Carbon will become "Lazy" — it will never be instantiated by the php-framework unless I explicitly request it via the $dates or $casts arrays. I will eliminate the reliance on the php-framework's runtime $dateFormat variable and grammar queries by baking the formats directly into the base Kernel Model. In MacropaySolutions\Kernel\Database\Obvious\Model: ⚙️ 2. Kernel Changes (The Core Traits) I will directly modify the php-framework's core traits in MacropaySolutions\Kernel\Database\Obvious\Concerns\ to stop using $this->freshTimestamp() and instead use native PHP \date() paired with the new Kernel constants. In HasTimestamps.php (updateTimestamps method): PHP public function updateTimestamps() } In SoftDeletes.php (runSoftDelete method): PHP protected function runSoftDelete() } 🏗️ 3. Kernel Changes (The Schema Blueprint) To ensure the database does not perform hidden timezone conversions on my fast PHP strings, I will modify the Schema Builder so that the migration helpers generate DATETIME columns instead of TIMESTAMP columns. In MacropaySolutions\Kernel\Database\Schema\Blueprint: 🚀 The Result (Why this will be genius) Update. datetime cast is gone. |
|
Document purpose of rate limit which does not replace server rate limits. See https://marius-ciclistu.medium.com/the-hidden-cost-of-fast-rejections-why-framework-boot-time-is-sinking-your-infrastructure-f2298ce4bc86 |
|
The http client should also be purged. Guzzle or curl can be used instead. |
|
Morphable relations maybe? I am not sure yet. I never used them because they are slower than normal relations. RetrieveQL does not support them anyway. |
|
Removing custom casts allowed the model serialization to be improved https://macropay-solutions.github.io/php-framework-docs/1.x/obvious.html#model-serialization-and-state-freezing |
|
Container optimization to avoid double failure public function build($concrete, array $parameters = []): mixed
{
if ($concrete instanceof Closure) {
return $concrete($this, $parameters);
}
$parameters = (
[] === BoundMethod::getAndCachePrecompiledAutoWiringClassMethodParametersMapForClassAndMethod(
\ltrim($concrete, '\\'),
'__construct'
)
) ? [] : ($parameters === [] || !\array_is_list($parameters) ?
\array_values(BoundMethod::getConstructDependencies(
$this,
$concrete,
$parameters
)) : $parameters);
// Track the class for autowiring discovery BEFORE we instantiate
BoundMethod::addToClassesFqnsToCacheForAutowire($concrete);
try {
// FAST PATH: This is all you need.
return new $concrete(...$parameters);
} catch (\Error $e) {
// ONLY catch native PHP fatal errors (e.g., trying to instantiate an interface).
// Let standard Application Exceptions bubble up instantly!
try {
$reflector = new ReflectionClass($concrete);
} catch (ReflectionException $e2) {
throw new BindingResolutionException("Target class [$concrete] does not exist.", 0, $e2);
}
if (!$reflector->isInstantiable()) {
$this->notInstantiable($concrete);
}
throw $e;
} |
|
The Application's methods that are there just to comply with the contract but are doing nothing, should be removed together with the contract method definition. |
|
Model::where should be removed in favor of Model::query()->where |
|
The through convention from __call should also be removed together with the actual through method and functionality. Also these should be history in obvious builder User::whereEmail($email); |
|
QueriesRelationships trait has these magic methods
|
|
Some methods from model were made public and strictly typed The model no longer forwards its calls to the builder implicitly. |
|
Model local scopes should not polute the model with methods so, replace dynamic method construction with a map lookup: public function hasNamedScope($scope)
{
return isset($this->segregatedScopesMap()[$scope]);
}
public function callNamedScope(
string $scope,
array $parameters = []
): Builder {
return $this->segregatedScopesMap()[$scope](...$parameters);
}Map in model: protected function segregatedScopesMap(): array
{
return [
'active' => static function (Builder $obviousBuilder): Builder {
return $obviousBuilder->where('active', 1);
},
];
}The builder should stop handling scopes via __call. |
|
Fix arbitrary command injection in Scheduler and Queue Worker https://github.com/macropay-solutions/php-kernel/releases/tag/1.0.0-RC-63 |
|
Updated Model's properties A and R from private to protected to allow this php 8.4 magic bypass : https://marius-ciclistu.medium.com/eliminate-magic-property-access-for-model-attributes-and-relations-in-obvious-model-orm-6d5ba666e786 |
|
This qualifies as a breaking change also https://marius-ciclistu.medium.com/php-framework-from-macropay-solutions-prevents-validation-rule-injection-37c587e38201 |
|
https://github.com/macropay-solutions/php-kernel/releases/tag/1.0.0-RC-70 https://github.com/macropay-solutions/php-kernel/releases/tag/1.0.0-RC-71 https://github.com/macropay-solutions/php-kernel/releases/tag/1.0.0-RC-72 Refactor: Enforce strict Model architecture, eliminate magic forwarding, and decouple HTTP routing This commit introduces a fundamental architectural shift to the Macropay-Solutions PHP-Framework, officially transitioning the ORM from a legacy "God Class" pattern to a strictly typed, explicit engine. It removes deep-rooted magic, enforces PHP 8 type contracts globally, and cleanly segregates the Database and HTTP layers. Key Architectural Changes: Eliminated Query Builder Proxying: Removed the ForwardsCalls trait and gutted the Model::__call() fallback. Magic queries (e.g., $model->where() or static Model::find()) are no longer supported. Developers must now explicitly request the builder via Model::query()->where(). Decoupled ORM from the HTTP Layer: Deleted the UrlRoutable interface and eradicated all route-binding leakage from the ORM. Gutted implicit model binding resolution from Broadcaster and UrlGenerator, enforcing explicit scalar ID passing across the framework. Enforced Strict Interface Contracts: Upgraded foundational interfaces (Arrayable, Jsonable, CanBeEscapedWhenCastToString) with strict PHP 8 return types (: array, : string, : static). Systematically propagated these strict contracts across all implementing classes, including Paginator, Collection, Fluent, MessageBag, and ValidatedInput. Optimized Internal Execution: Replaced legacy call_user_func() mechanisms with highly optimized direct static closure invocations (e.g., (static::$callback)($this, $keys)). Bypassed Magic Method Overhead: Refactored internal framework logic (such as unique ID generation in HasUniqueIds) to use explicit getAttributeValue() and setAttribute() methods, bypassing expensive __get/__set magic method loops. Secured Serialization Accuracy: Changed the default value of HasAttributes::$snakeAttributes to false to prevent the hidden mutation of camelCase column names during array/JSON serialization. Breaking Changes: UrlRoutable is removed; implicit model injection in controllers and broadcast channels requires manual ID fetching. Direct builder method calls on model instances will now throw a BadMethodCallException. Always chain from ->query(). 1000 lines deleted |
|
Macros are not inheritable... Child classes registered in DI can't inherit from multiple packages... that leads to manual registration via traits. Maybe I can find a better and faster solution that fixes both issues. |
|
Console |
|
https://packagist.org/packages/macropay-solutions/php-kernel#1.0.0-RC-98 unserialize allowed_classes false and base64_decode strict true To avoid POI and RCE |
|
Patched timing attack on app key rotation in rc 104 and fixed the cipher rotation also. |
|
Rip out most of the magic and replace it with ->to() and ->toStatic(). For manager classes the magic remains. https://packagist.org/packages/macropay-solutions/php-kernel#1.0.0-RC-105 Release Notes: Architectural Strictness & Magic RemovalOverviewThis release introduces a massive architectural shift focused on strict typing, static analysis compliance, and the eradication of runtime "magic." By removing dynamic method interception and higher-order proxies, the framework provides more transparency to IDEs and static analysis tools (like PHPStan and Psalm). 💥 Breaking Changes
✨ New Features & Enhancements
|
|
The deferred macro logic can improve bindings registration. This is a high level idea. Register Explicit Bindings could be kept for middlewares etc. instead of being removed. /**
// * To avoid calls to
// * @see \MacropaySolutions\Framework\Concerns\RoutesRequests::routeMiddleware()
// * Note that you can use the middleware FQN on a route without declaring its alias here!
// */
// $this->routeMiddleware['decorate-' . ResourceClass::RESOURCE_NAME] =
// \App\Http\Middleware\ResourceClassDecorator::class;
// $this->foundRouteMiddleware['decorate-' . ResourceClass::RESOURCE_NAME] =
// \App\Http\Middleware\ResourceClassDecorator::class;
}Patch 1: kernel/Container/Container.php Diff
--- kernel/Container/Container.php
+++ kernel/Container/Container.php
@@ -201,44 +201,17 @@
/**
* Register a binding with the container.
- *
- * @param string $abstract
- * @param \Closure|string|null $concrete
- * @param bool $shared
- * @return void
- *
- * @throws \TypeError
+ *
+ * Explicit bindings accept a class FQCN string, a static Closure, or a static array callable (e.g. [Factory::class, 'make']).
+ * Array callables MUST use a class string as element 0 to remain state-free and memory-friendly under OPcache.
*/
- public function bind($abstract, $concrete = null, $shared = false)
+ public function bind(string $abstract, array|string|\Closure|null $concrete = null, bool $shared = false): void
{
$this->dropStaleInstances($abstract);
- // If the factory is not a Closure, it means it is just a class name which is
- // bound into this container to the abstract type and we will just wrap it
- // up inside its own Closure to give us more convenience when extending.
- if (!$concrete instanceof Closure) {
- if (!\is_string($concrete ??= $abstract)) {
- throw new TypeError(
- self::class . '::bind(): Argument #2 ($concrete) must be of type Closure|string|null'
- );
- }
-
- $concrete = $this->getClosure($abstract, $concrete);
- }
+ $concrete ??= $abstract; // can be inlined bellow
$this->bindings[$abstract] = ['concrete' => $concrete, 'shared' => $shared];
@@ -250,43 +223,6 @@
}
- /**
- * Set all the container bindings that should be registered when the app is instantiated
- * @see \MacropaySolutions\Kernel\Container\Container::getClosure for Closure format
- * Must set array shape:
- * [
- * "{$abstractString}" => [
- * 'concrete' => \Closure,
- * 'shared' => bool
- * ],
- * ]
- */
- protected function registerExplicitBindingsMap(): void
- {
-// $this->bindings = [
-// \MacropaySolutions\Kernel\Http\Request::class => [
-// 'concrete' => static function (
-// \MacropaySolutions\Kernel\Contracts\Container\Container $container,
-// array $parameters = []
-// ): \MacropaySolutions\Kernel\Http\Request {
-// return $container->resolve(
-// \App\Requests\Request::class, // your child class
-// $parameters,
-// false
-// );
-// },
-// 'shared' => false
-// ],
-// ];
- }
- /**
- * Get the Closure to be used when building a type.
- *
- * @param string $abstract
- * @param string $concrete
- * @return \Closure
- */
- protected function getClosure($abstract, $concrete)
- {
- return static function ($container, $parameters = []) use ($abstract, $concrete) {
- if ($abstract == $concrete) {
- return $container->build($concrete);
- }
-
- return $container->resolve(
- $concrete,
- $parameters,
- false // raiseEvents
- );
- };
- }
/**
* Determine if the container has a method binding.
@@ -772,7 +708,7 @@
*/
protected function isBuildable($concrete, $abstract)
{
- return $concrete === $abstract || $concrete instanceof Closure;
+ return $concrete === $abstract || \is_array($concrete) || $concrete instanceof \Closure;
}
/**
@@ -782,9 +718,9 @@
* @throws \MacropaySolutions\Kernel\Contracts\Container\CircularDependencyException
* @throws ReflectionException
*/
- public function build(\Closure|string $concrete, array $parameters = []): mixed
+ public function build(array|string|\Closure $concrete, array $parameters = []): mixed
{
- if ($concrete instanceof Closure) {
+ if (\is_array($concrete) || $concrete instanceof \Closure) {
return $concrete($this, $parameters); // fails at build time if invalid
}
@@ -1071,7 +1007,11 @@
public function offsetSet($offset, $value): void
{
- $this->bind($offset, $value instanceof Closure ? $value : static fn() => $value);
+ if (!\is_array($value) && !\is_string($value) && !$value instanceof Closure) {
+ $this->instance($offset, $value);
+
+ return;
+ }
+
+ $this->bind($offset, $value);
}Patch 2: framework/Application.php Diff
--- framework/Application.php
+++ framework/Application.php
@@ -62,7 +62,25 @@
* The container's bindings.
*
* @var array[]
*/
- protected $bindings = [];
+ protected $bindings = [
+// \MacropaySolutions\Kernel\Http\Request::class => [
+// 'concrete' => [Class::class, 'resolve']
+// 'shared' => false
+// ],
+ ];
/**
* Create a new Framework application instance.
@@ -104,6 +122,4 @@
static::setInstance($this);
- $this->registerExplicitBindingsMap();@
$this->instance('app', $this);Patch 3: app/Application.php & New Factory Class
Create this file to hold static methods for custom complex resolutions: <?php
namespace App\Factories;
use App\Exceptions\Handler;
use App\Console\Kernel;
use MacropaySolutions\CrufdWizard\Helpers\GeneralHelper;
use MacropaySolutions\CrufdWizard\Responses\DecoratableJsonResponse;
use MacropaySolutions\Kernel\Http\JsonResponse;
class ContainerBindingsFactory
{
public static function createExceptionHandler(): Handler
{
return new Handler();
}
public static function createConsoleKernel($app): Kernel
{
return new Kernel($app);
}
public static function createJsonResponse($app, array $parameters): JsonResponse
{
if (
false === ($parameters['json'] ?? $parameters[4] ?? false)
&& \in_array($code = (string)($parameters['status'] ?? $parameters[1] ?? '200'), ['200', '201', '202'], true)
&& \is_string(
$decoratorFlag = ($request = $app['request'])->header(
GeneralHelper::JSON_RESPONSE_AS_ARRAY_FOR_DECORATION_IN_REQUEST_ATTRIBUTES
)
)
&& '' !== (string)($appKey = $app['config']->get('app.key'))
&& \hash_equals(
$decoratorFlag,
\hash_hmac('sha256', GeneralHelper::JSON_RESPONSE_AS_ARRAY, $appKey)
)
) {
if (\is_array($parameters['data'] ?? null)) {
$request->attributes->set(GeneralHelper::JSON_RESPONSE_AS_ARRAY, $parameters['data']);
return new DecoratableJsonResponse(
[],
$code,
$parameters['headers'] ?? [],
$parameters['options'] ?? JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES,
false
);
}
if (\is_array($parameters[0] ?? null)) {
$request->attributes->set(GeneralHelper::JSON_RESPONSE_AS_ARRAY, $parameters[0]);
return new DecoratableJsonResponse(
[],
$code,
$parameters[2] ?? [],
$parameters[3] ?? JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES,
false
);
}
}
return new JsonResponse(
$parameters['data'] ?? $parameters[0] ?? null,
$parameters['status'] ?? $parameters[1] ?? 200,
$parameters['headers'] ?? $parameters[2] ?? [],
$parameters['options'] ?? $parameters[3] ?? JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES,
$parameters['json'] ?? $parameters[4] ?? false,
);
}
}
Diff
--- app/Application.php
+++ app/Application.php
@@ -28,6 +28,26 @@
];
+ /**
+ * Pre-compiled bindings array.
+ * Replaces registerExplicitBindingsMap(). Loaded directly into memory by OPcache.
+ *
+ * @var array
+ */
+ protected $bindings = [
+ \MacropaySolutions\Kernel\Contracts\Debug\ExceptionHandler::class => [
+ 'concrete' => [\App\Factories\ContainerBindingsFactory::class, 'createExceptionHandler'],
+ 'shared' => true,
+ ],
+ \MacropaySolutions\Kernel\Contracts\Console\Kernel::class => [
+ 'concrete' => [\App\Factories\ContainerBindingsFactory::class, 'createConsoleKernel'],
+ 'shared' => true,
+ ],
+ JsonResponse::class => [
+ 'concrete' => [\App\Factories\ContainerBindingsFactory::class, 'createJsonResponse'],
+ 'shared' => false,
+ ],
+ ];
/**
* The available container bindings and their respective load methods.
@@ -204,74 +224,6 @@
}
- /**
- * Set all the container bindings that should be registered when the app is instantiated
- * @see \MacropaySolutions\Kernel\Container\Container::getClosure for Closure format
- * @see static::registerContainerAliases to handle alias changes if impacted by this function
- * Must set array shape:
- * [
- * "{$abstractString}" => [
- * 'concrete' => \Closure,
- * 'shared' => bool
- * ],
- * ]
- */
- protected function registerExplicitBindingsMap(): void
- {
- $this->bindings = [
-// \ParentFqn::class => [
-// 'concrete' => static function (
-// \MacropaySolutions\Kernel\Contracts\Container\Container $container,
-// array $parameters = []
-// ): \MacropaySolutions\Kernel\Http\Request {
-// return $container->resolve(
-// \ChildFqn::class, // your child class
-// $parameters,
-// false
-// );
-// },
-// 'shared' => false
-// ],
- \MacropaySolutions\Kernel\Contracts\Debug\ExceptionHandler::class => [
- 'concrete' => static fn(): \App\Exceptions\Handler => new \App\Exceptions\Handler(),
- 'shared' => true
- ],
- \MacropaySolutions\Kernel\Contracts\Console\Kernel::class => [
- 'concrete' => static fn($app): \App\Console\Kernel => new \App\Console\Kernel($app),
- 'shared' => true
- ],
- JsonResponse::class => [
- 'concrete' => static function ($app, $parameters): JsonResponse {
- // ... old inline closure logic ...
- },
- 'shared' => false
- ],
- ];
- }
/**
* Register the core container aliases.Notice that ->resolve will not be available because is protected in container. Closures would still work but will be slower even if defined static. That is why caching the bindings is not an option. Deferred Providers like the MailServiceProvider can bind things in their register even if they will not be registered when the app boots but only when they are required. This reminds me. |
|
1.0.0-RC-125 |
|
https://packagist.org/packages/macropay-solutions/php-kernel-dev#1.0.0-RC-34 PHP storm IDE ctrl-click on things like // getFiltered is ctrl+click-able and autocomplet-able
\app('request')->getFiltered('key');
\di('request')->getFiltered('key');
\app()->make('request')->getFiltered('key');via composer plugin dump-autoload event.
|






Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
I am looking for suggestions and ideas of what new improvements could I/we do before V1 release from the end of 2026, that require breacking changes.
Currently the php ecosystem is in RC version and the time left can be used proactively.
What was accomplished so far can be seen in the releases and commits sections of each project. If you like fabulas, you can read a high level metaphoric presentation in The API Grand Prix.
The Active record and global helpers will stay because RetrieveQL relies on them.
The documentation is up to date with the latest code changes: https://macropay-solutions.github.io/php-framework-docs/1.x/.
All reactions