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
43 changes: 43 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -1,2 +1,45 @@
/.gitattributes export-ignore
/captainhook.json export-ignore
/composer.lock export-ignore
/tests export-ignore
/.agents export-ignore
/.codex export-ignore
/.env export-ignore
/.env.* export-ignore
/.env.example -export-ignore
/.github export-ignore
/.idea export-ignore
/.phpunit.cache export-ignore
/.psalm-cache export-ignore
/.route-cache-* export-ignore
/.vscode export-ignore
/.windsurf export-ignore
/app/Actions export-ignore
/app/Domain export-ignore
/app/Http/Middleware export-ignore
/app/Http/Requests export-ignore
/app/Http/Resources export-ignore
/app/Jobs export-ignore
/app/Listeners export-ignore
/app/Models export-ignore
/app/Policies export-ignore
/app/Providers export-ignore
/app/Services export-ignore
/app/Support export-ignore
/bin export-ignore
/bootstrap/cache/config/*.php export-ignore
/bootstrap/cache/console/*.php export-ignore
/bootstrap/cache/modules.php export-ignore
/bootstrap/cache/routes/*.php export-ignore
/database export-ignore
/resources export-ignore
/storage/app export-ignore
/storage/cache/* export-ignore
/storage/cache/.gitignore -export-ignore
/storage/logs/* export-ignore
/storage/logs/.gitignore -export-ignore
/storage/sessions/* export-ignore
/storage/sessions/.gitignore -export-ignore
/storage/uploads/* export-ignore
/storage/uploads/.gitignore -export-ignore
/vendor export-ignore
45 changes: 45 additions & 0 deletions .github/workflows/security-standards.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
name: "Security & Standards"

on:
schedule:
- cron: "0 0 * * 0"
push:
branches: [ "main", "master" ]
pull_request:
branches: [ "main", "master", "develop", "development" ]

jobs:
phpforge:
uses: infocyph/phpforge/.github/workflows/security-standards.yml@main
permissions:
security-events: write
actions: read
contents: read
with:
php_versions: '["8.4","8.5"]'
dependency_versions: '["prefer-lowest","prefer-stable"]'
php_extensions: "pcntl"
composer_flags: ""
phpstan_memory_limit: "1G"
psalm_threads: "1"
run_analysis: true
run_svg_report: true
fail_on_skipped_tests: true
run_clean_install: true
benchmark_composer_script: ""
benchmark_result_file: ""
benchmark_baseline_file: ""
benchmark_max_regression_percent: 2
benchmark_stable_environment: false
enable_redis_service: false
enable_valkey_service: false
enable_memcached_service: false
enable_postgres_service: false
enable_mysql_service: false
enable_scylladb_service: false
enable_elasticsearch_service: false
enable_mongodb_service: false
service_db_name: "infbyte"
service_db_user: "infbyte"
service_db_password: "infbyte"
artifact_retention_days: 61
28 changes: 21 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,13 @@ starter code.
```bash
composer create-project infocyph/infbyte my-app
cd my-app
cp .env.example .env
php -S localhost:8000 -t public
```

Composer creates `.env` from `.env.example` and generates a random
authentication token secret without replacing an existing environment file.
Infbyte 1.x supports PHP 8.4 and newer; its CI verifies PHP 8.4 and 8.5.

The starter application exposes:

- `GET /api/health`
Expand Down Expand Up @@ -84,7 +87,16 @@ free to use unrelated names.
`php infbyte list` places Foundation's framework commands under `System`,
package capability commands under their capability group, and application
commands under the first namespace segment in their route name, such as
`reports` for `reports:daily`.
`reports` for `reports:daily`. `php infbyte --version` reports the installed
Foundation runtime version because Composer-created root projects do not retain
the skeleton package version. The displayed CLI application name comes from
`APP_NAME` in `.env` (with process environment values taking precedence) and
falls back to `infbyte` when it is missing or empty.

Start the local development server with `php infbyte serve`. It serves the
configured public directory at `http://127.0.0.1:8000`; use `--host` and
`--port` to select another bind address. PHP's built-in server is for local
development only, not production deployment.

Create application artifacts only when the project needs them:

Expand Down Expand Up @@ -131,11 +143,13 @@ php infbyte optimize
php infbyte optimize:clear
```

This builds the sharded configuration cache, selected route matcher cache, and
compiled command manifest. Command descriptors remain lazy and are stored
directly in `bootstrap/cache/console/` beside the manifest. `optimize:clear`
removes all three cache types. Cache compilation may do more work so web
requests and command dispatch do less.
This builds the sharded configuration cache, selected route matcher cache,
compiled command metadata, schedule manifest, and installed-module manifest.
Command descriptors remain lazy and are stored directly in
`bootstrap/cache/console/` beside the command manifest. `optimize:clear`
removes every generated application cache, including every route matcher
layout. Cache compilation may do more work so web requests and command
dispatch do less.

## Modules and published configuration

Expand Down
77 changes: 77 additions & 0 deletions bootstrap/install.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
<?php

declare(strict_types=1);

$basePath = dirname(__DIR__);
$environmentPath = $basePath . '/.env';

if (is_file($environmentPath)) {
fwrite(STDOUT, "[INFO] Kept existing .env file.\n");

return;
}

$examplePath = $basePath . '/.env.example';
$environment = file_get_contents($examplePath);

if (!is_string($environment)) {
throw new RuntimeException('Unable to read .env.example.');
}

$replacements = 0;
$environment = preg_replace(
'/^AUTH_TOKEN_SECRET=.*$/m',
'AUTH_TOKEN_SECRET=' . bin2hex(random_bytes(32)),
$environment,
1,
$replacements,
);

if (!is_string($environment) || $replacements !== 1) {
throw new RuntimeException('Unable to provision AUTH_TOKEN_SECRET in .env.');
}

$previousUmask = umask(0077);

try {
$handle = fopen($environmentPath, 'x');
} finally {
umask($previousUmask);
}

if (!is_resource($handle)) {
throw new RuntimeException('Unable to create .env.');
}

try {
$length = strlen($environment);
$written = 0;

while ($written < $length) {
$bytes = fwrite($handle, substr($environment, $written));
if ($bytes === false || $bytes === 0) {
throw new RuntimeException('Unable to write the complete .env file.');
}
$written += $bytes;
}

if (!fflush($handle)) {
throw new RuntimeException('Unable to flush the complete .env file.');
}
} catch (Throwable $exception) {
fclose($handle);
if (is_file($environmentPath)) {
unlink($environmentPath);
}

throw $exception;
}

fclose($handle);
if (!chmod($environmentPath, 0600)) {
unlink($environmentPath);

throw new RuntimeException('Unable to restrict .env permissions to the project owner.');
}

fwrite(STDOUT, "[OK] Created .env with a random authentication token secret.\n");
21 changes: 18 additions & 3 deletions bootstrap/providers.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,22 @@
declare(strict_types=1);

return [
'common' => [],
'web' => [],
'console' => [],
'common' => [
/**
* Usage:
* \App\Providers\SharedServiceProvider::class,
*/
],
'web' => [
/**
* Usage:
* \App\Providers\WebServiceProvider::class,
*/
],
'console' => [
/**
* Usage:
* \App\Providers\ConsoleServiceProvider::class,
*/
],
];
7 changes: 4 additions & 3 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,11 @@
"type": "project",
"require": {
"php": ">=8.4",
"infocyph/foundation": "dev-main@dev"
"infocyph/foundation": "^1.2"
},
"require-dev": {
"infocyph/phpforge": "dev-main@dev"
},
"minimum-stability": "dev",
"prefer-stable": true,
"autoload": {
"psr-4": {
"App\\": "app/"
Expand All @@ -30,5 +28,8 @@
},
"optimize-autoloader": true,
"sort-packages": true
},
"scripts": {
"post-create-project-cmd": "@php bootstrap/install.php"
}
}
12 changes: 10 additions & 2 deletions infbyte
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,23 @@

declare(strict_types=1);

use Composer\InstalledVersions;
use Infocyph\Foundation\Application\Application;
use Infocyph\Foundation\Console\FoundationConsole;
use Infocyph\Foundation\Config\EnvironmentLoader;

require __DIR__ . '/vendor/autoload.php';

$basePath = __DIR__;
$commandManifest = $basePath . '/bootstrap/cache/console/commands.php';
$commands = [];

new EnvironmentLoader()->load($basePath);
$applicationName = trim(env_string('APP_NAME', 'infbyte'));
if ($applicationName === '') {
$applicationName = 'infbyte';
}

if (!is_file($commandManifest)) {
$commands = require $basePath . '/routes/console.php';

Expand All @@ -34,8 +42,8 @@ $console = FoundationConsole::create(

return $application;
},
name: 'infbyte',
version: 'dev-main',
name: $applicationName,
version: InstalledVersions::getPrettyVersion('infocyph/foundation') ?? 'dev-main',
commands: $commands,
commandManifest: $commandManifest,
);
Expand Down
7 changes: 6 additions & 1 deletion routes/console.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,9 @@

declare(strict_types=1);

return [];
return [
/**
* Usage:
* 'reports:daily' => \App\Console\Commands\Reports\DailyCommand::class,
*/
];
43 changes: 41 additions & 2 deletions tests/Feature/RouteCacheCliTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,38 @@

declare(strict_types=1);

use Composer\InstalledVersions;
use Infocyph\Foundation\Foundation;
use Infocyph\Foundation\Routing\RouteCachePath;

it('uses the environment application name and reports the Foundation runtime version', function (): void {
$root = dirname(__DIR__, 2);
[$exitCode, $output] = runInfbyteCommand([
PHP_BINARY,
$root . '/infbyte',
'--version',
], ['APP_NAME' => 'Acme Console']);

expect($exitCode)->toBe(0)
->and($output)->toBe(
'Acme Console ' . (InstalledVersions::getPrettyVersion('infocyph/foundation') ?? 'dev-main'),
);
});

it('falls back to infbyte when the environment application name is empty', function (): void {
$root = dirname(__DIR__, 2);
[$exitCode, $output] = runInfbyteCommand([
PHP_BINARY,
$root . '/infbyte',
'--version',
], ['APP_NAME' => '']);

expect($exitCode)->toBe(0)
->and($output)->toBe(
'infbyte ' . (InstalledVersions::getPrettyVersion('infocyph/foundation') ?? 'dev-main'),
);
});

it('builds and clears route cache through the infbyte cli wrapper', function (): void {
$root = dirname(__DIR__, 2);
$cacheFile = rtrim(sys_get_temp_dir(), DIRECTORY_SEPARATOR)
Expand Down Expand Up @@ -198,11 +227,21 @@

/**
* @param list<string> $arguments
* @param array<string, string> $environment
* @return array{0:int,1:string}
*/
function runInfbyteCommand(array $arguments): array
function runInfbyteCommand(array $arguments, array $environment = []): array
{
$command = implode(' ', array_map(
$command = '';
foreach ($environment as $key => $value) {
if (preg_match('/^[A-Z_][A-Z0-9_]*$/D', $key) !== 1) {
throw new InvalidArgumentException(sprintf('Invalid environment variable name: %s', $key));
}

$command .= $key . '=' . escapeshellarg($value) . ' ';
}

$command .= implode(' ', array_map(
static fn(string $argument): string => escapeshellarg($argument),
$arguments,
)) . ' 2>&1';
Expand Down
Loading
Loading