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
12 changes: 12 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,15 @@ Thumbs.db
.env.*
!.env.example

# Xdebug
._icons/*
._js/*
._css/*
.phpunit.cache/

# PHPUnit Coverage Reports
_coverage/
*.html
!resources/**/*.html

tree
1,301 changes: 1,301 additions & 0 deletions phpunit.out

Large diffs are not rendered by default.

49 changes: 38 additions & 11 deletions src/Application.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
use BareMetalPHP\Support\Config;
use BareMetalPHP\Support\Facades\AliasLoader;
use BareMetalPHP\Support\Facades\Facade;
use BareMetalPHP\Config\Repository as ConfigRepository;

class Application
{
Expand All @@ -24,6 +25,8 @@ class Application

protected array $instances = [];

protected ?ConfigRepository $config = null;

/**
* @var ServiceProvider[]
*/
Expand Down Expand Up @@ -56,7 +59,7 @@ public static function getInstance(): ?Application
/**
* Set the globally accessible application instance
*/
public static function setInstance(Application $app): void
public static function setInstance(?Application $app): void
{
static::$instance = $app;
}
Expand Down Expand Up @@ -171,10 +174,16 @@ protected function getConstructor(ReflectionClass $reflection): ?ReflectionMetho

protected function resolveParameter(ReflectionParameter $param): mixed
{
// If parameter has a default value, use it
if ($param->isDefaultValueAvailable()) {
return $param->getDefaultValue();
}

$type = $param->getType();

// If no type or built-in type without default, we can't resolve it
if (! $type || $type->isBuiltin()) {
throw new \RuntimeException("Cannot resolve parameter {$param->getName()}");
throw new \RuntimeException("Cannot resolve parameter {$param->getName()} of class " . ($param->getDeclaringClass() ? $param->getDeclaringClass()->getName() : 'unknown'));
}

return $this->make($type->getName());
Expand Down Expand Up @@ -206,6 +215,11 @@ public function boot(): void
return;
}

// Make sure the Facade base class knows about the app
// __before__ anything else might resolve facades.
Facade::setFacadeApplication($this);

// Boot all service providers
foreach ($this->serviceProviders as $provider) {
$provider->boot();
}
Expand All @@ -214,22 +228,15 @@ public function boot(): void
// Register facade aliases if completed.
$aliases = [];

try {
// If config system is not yet initialized, this will just throw
$aliases = Config::get('app.aliases', []);
} catch (\Throwable $e) {
// Fail quietly if config isn't available for some reason.
$aliases = [];
if ($this->config !== null) {
$aliases = $this->config->get('app.aliases', []);
}

if (! empty($aliases)) {
$loader = AliasLoader::getInstance($aliases);
$loader->register();
}

// Let the Facade base know about the app instance.
Facade::setFacadeApplication($this);

$this->booted = true;
}

Expand All @@ -242,4 +249,24 @@ public function isBooted(): bool
{
return $this->booted;
}

public function setConfig(ConfigRepository $config): void
{
$this->config = $config;
}

public function config(?string $key = null, mixed $default = null): mixed
{
if (! $this->config) {
return $default;
}

if ($key === null) {
return $this->config;
}

return $this->config->get($key, $default);
}


}
85 changes: 85 additions & 0 deletions src/Config/Repository.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
<?php

declare(strict_types= 1);

namespace BareMetalPHP\Config;

class Repository
{
/**
* @var array<string, mixed>
*/
protected array $items = [];

/**
* @param array<string, mixed> $items
*/
public function __construct(array $items = [])
{
$this->items = $items;
}

/**
* Load config files from a directory.
*
* @param string $configPath
* @return Repository
*/
public static function fromPath(string $configPath): self
{
$items = [];

foreach (glob(rtrim($configPath, "/"). '/*.php') as $file) {
$name = basename($file, '.php'); // app.php -> "app"
$items[$name] = require $file;
}

return new self($items);
}

public function all(): array
{
return $this->items;
}

public function has(string $key): bool
{
return $this->get($key, '__missing__') != '__missing__';
}

public function get(string $key, mixed $default = null): mixed
{
$segments = explode('.', $key);

$value = $this->items;

foreach ($segments as $segment) {
if (! is_array($value) || ! array_key_exists($segment, $value)) {
return $default;
}

$value = $value[$segment];
}

return $value;
}

public function set(string $key, mixed $value): void
{
$segments = explode('.', $key);

$array = $this->items;

while (count($segments) > 1) {
$segment = array_shift($segments);

if (! isset($array[$segment]) || ! is_array($array[$segment])) {
$array[$segment] = [];
}

$array =& $array[$segment];
}

$array[array_shift($segments)] = $value;
}
}
2 changes: 1 addition & 1 deletion src/Database/Builder.php
Original file line number Diff line number Diff line change
Expand Up @@ -221,7 +221,7 @@ public function get(): Collection

// Simple eager loading: trigger each relationship once per model so that
// subsequent access does not hit the database.
if (!empty($this->eagerLoad) ** method_exists($class, 'eagerLoadCollection')) {
if (!empty($this->eagerLoad) && method_exists($class, 'eagerLoadCollection')) {
$class::eagerLoadCollection($collection, array_keys($this->eagerLoad));
}

Expand Down
8 changes: 6 additions & 2 deletions src/Database/Relations/BelongsToMany.php
Original file line number Diff line number Diff line change
Expand Up @@ -197,8 +197,8 @@ public function detach(int|array|null $ids = null): int
}

$placeholders = implode(',', array_fill(0, count($ids), '?'));
$sql = "DELETE FROM {$quotedPivotTable} WHERE {$quotedForeignPivotKey} = :parent_id AND {$quotedRelatedPivotKey} IN ({$placeholders})";
$bindings = array_merge(['parent_id' => $parentValue], $ids);
$sql = "DELETE FROM {$quotedPivotTable} WHERE {$quotedForeignPivotKey} = ? AND {$quotedRelatedPivotKey} IN ({$placeholders})";
$bindings = array_merge([$parentValue], $ids);
$stmt = $pdo->prepare($sql);
$stmt->execute($bindings);

Expand All @@ -211,6 +211,10 @@ public function detach(int|array|null $ids = null): int
public function sync(array $ids, bool $detaching = true): array
{
$current = $this->getPivotIds();
// Convert current IDs to integers for comparison (SQLite returns strings)
$current = array_map('intval', $current);
// Ensure input IDs are integers
$ids = array_map('intval', $ids);

$detach = [];
$attach = [];
Expand Down
1 change: 1 addition & 0 deletions src/Frontend/AssetManager.php
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,7 @@ public function viteClient(): string

// When using React, inject React preamble
$framework = Config::get('frontend.framework', null);
$reactPreamble = '';

if ($framework === 'react') {
$reactPreamble = <<<HTML
Expand Down
32 changes: 32 additions & 0 deletions src/Http/Response.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@

namespace BareMetalPHP\Http;

use BareMetalPHP\Application;
use BareMetalPHP\Serialization\Serializer;

class Response
{
public function __construct(
Expand Down Expand Up @@ -53,4 +56,33 @@ public function getHeaders(): array
{
return $this->headers;
}

public static function json(
mixed $data,
int $status = 200,
array $headers = [],
array $context = []
): self {
$app = Application::getInstance();
$content = null;

if ($app) {
try {
/** @var Serializer $serializer */
$serializer = $app->make(Serializer::class);
$content = $serializer->serialize($data, 'json', $context);
} catch (\Throwable $e) {
// fallback to bare json_encode if the serializer is not available
$content = json_encode($data);
}
} else {
$content = json_encode($data);
}

$headers['Content-Type'] = $headers['Content-Type'] ?? 'application/json';

return new static((string) $content, $status, $headers);
}


}
12 changes: 11 additions & 1 deletion src/Providers/ConfigServiceProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
use BareMetalPHP\Support\Config;
use BareMetalPHP\Support\Env;
use BareMetalPHP\Support\ServiceProvider;
use BareMetalPHP\Config\Repository as ConfigRepository;

class ConfigServiceProvider extends ServiceProvider
{
Expand All @@ -22,8 +23,17 @@ public function register(): void

public function boot(): void
{
// Load configuration files
// Load configuration files using static Config class
Config::load();

// Create ConfigRepository from the loaded config data
// This unifies the two config systems - static Config and Repository
$configData = Config::all();
$configRepository = new ConfigRepository($configData);

// Register ConfigRepository in the container and application
$this->app->instance(ConfigRepository::class, $configRepository);
$this->app->setConfig($configRepository);
}

protected function loadEnvironmentFile(): void
Expand Down
27 changes: 27 additions & 0 deletions src/Providers/SerializationServiceProvider.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<?php

declare(strict_types=1);

namespace BareMetalPHP\Providers;

use BareMetalPHP\Support\ServiceProvider;
use BareMetalPHP\Serialization\Serializer;
use BareMetalPHP\Serialization\JsonEncoder;
use BareMetalPHP\Serialization\DefaultNormalizer;

class SerializationServiceProvider extends ServiceProvider
{
public function register(): void
{
$this->app->singleton(Serializer::class, function () {
return new Serializer(
normalizers: [
new DefaultNormalizer(),
],
encoders: [
new JsonEncoder(),
]
);
});
}
}
8 changes: 8 additions & 0 deletions src/Routing/RouteDefinition.php
Original file line number Diff line number Diff line change
Expand Up @@ -30,4 +30,12 @@ public function middleware(string|array $middleware): self
$this->router->setRouteMiddleware($this->method, $this->uri, $this->middleware);
return $this;
}

/**
* Get the URI for this route
*/
public function getUri(): string
{
return $this->uri;
}
}
Loading
Loading