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
18 changes: 18 additions & 0 deletions .env.local
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
APP_ENV=development
APP_DEBUG=true

FILE_STORAGE_DRIVER=local
FILE_STORAGE_LOCAL_PATH=storage/

DATABASE_DRIVER=sqlite
# DATABASE_HOST=localhost
# DATABASE_NAME=myapp
# DATABASE_CHARSET=utf8mb4
# DATABASE_USERNAME=root
# DATABASE_PASSWORD=

# AWS_ACCESS_KEY_ID=
# AWS_SECRET_ACCESS_KEY=
# AWS_DEFAULT_REGION=
# AWS_DEFAULT_ENDPOINT=
# AWS_BUCKET=
5 changes: 4 additions & 1 deletion Core/Container.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,17 @@

namespace Core;

use function array_key_exists;
use function call_user_func;

class Container
{
public function bind(string $key, callable $resolver): void
{
$this->bindings[$key] = $resolver;
}

public function resolve(string $key): mixed
public function resolve(string $key): callable
{
if (!array_key_exists($key, $this->bindings)) {
throw new \Exception("No matching binding found for '{$key}'");
Expand Down
9 changes: 9 additions & 0 deletions Core/FileStorage.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<?php declare(strict_types=1);

namespace Core;

interface FileStorage
{
public function put(string $path, string $content): bool;
public function get(string $path): ?string;
}
9 changes: 9 additions & 0 deletions Core/FileStorageDriver.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<?php declare(strict_types=1);

namespace Core;

enum FileStorageDriver: string
{
case LOCAL = 'local';
case S3 = 's3';
}
40 changes: 40 additions & 0 deletions Core/LocalStorage.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
<?php declare(strict_types=1);

namespace Core;

class LocalStorage implements FileStorage
{
public function __construct()
{
$this->basePath = __DIR__ . "/../{$_ENV['FILE_STORAGE_LOCAL_PATH']}";
}

public function put(string $path, string $content): bool
{
$savePath = "{$this->basePath}/{$path}";

if (!is_dir(dirname($savePath))) {
mkdir(dirname($savePath), 0777, true);
}

file_put_contents($savePath, $content);
return true;
}

public function get(string $path): ?string
{
$fullPath = "{$this->basePath}$path";

if (!file_exists($fullPath)) {
throw new \Exception("File not found: {$path}");
}

if (!is_readable($fullPath)) {
throw new \Exception("File not readable: {$path}");
}

return file_get_contents($fullPath);
}

protected string $basePath;
}
41 changes: 41 additions & 0 deletions Core/S3Storage.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
<?php declare(strict_types=1);

namespace Core;

use Aws\S3\S3Client;
use Aws\S3\Exception\S3Exception;

class S3Storage implements FileStorage
{
public function __construct(protected S3Client $client, protected string $bucket)
{
}

public function put(string $path, string $content): bool
{
try {
$this->client->putObject([
'Bucket' => $this->bucket,
'Key' => $path,
'Body' => $content,
]);
return true;
} catch (S3Exception $e) {
echo "Failed to upload file to S3: {$e->getMessage()}\n";
return false;
}
}

public function get(string $path): ?string
{
try {
$result = $this->client->getObject([
'Bucket' => $this->bucket,
'Key' => $path,
]);
return $result->get('Body')->getContents();
} catch (S3Exception $e) {
throw new \Exception("Failed to get file from S3: {$e->getMessage()}");
}
}
}
35 changes: 35 additions & 0 deletions Core/Storage.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
<?php declare(strict_types=1);

namespace Core;

use Aws\S3\S3Client;
use Exception;

class Storage
{
/**
* @throws Exception
*/
public static function resolve(): FileStorage
{
$driver = FileStorageDriver::from($_ENV['FILE_STORAGE_DRIVER']);

if ($driver === FileStorageDriver::LOCAL) {
return new LocalStorage();
} else if ($driver === FileStorageDriver::S3) {
$client = new S3Client([
'version' => 'latest',
'region' => $_ENV['AWS_DEFAULT_REGION'],
'endpoint' => $_ENV['AWS_DEFAULT_ENDPOINT'],
'credentials' => [
'key' => $_ENV['AWS_ACCESS_KEY_ID'],
'secret' => $_ENV['AWS_SECRET_ACCESS_KEY']
]
]);

return new S3Storage($client, $_ENV['AWS_BUCKET']);
}

throw new Exception('Invalid storage method.');
}
}
83 changes: 75 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,9 @@ A minimal, dependency-light PHP microframework skeleton. This repository provide
- [App container API](#app-container-api)
- [Accessing the Database](#accessing-the-database)
- [Routes and Router](#routes-and-router)
- [Environment variables](#environment-variables)
- [Configuration](#configuration)
- [File Storage API](#file-storage-api)
- [Testing](#testing)
- [Contributing](#contributing)
- [License](#license)
Expand All @@ -22,10 +24,15 @@ A minimal, dependency-light PHP microframework skeleton. This repository provide
Top-level files and folders in the repository:

- .gitignore
- Core/ — core framework classes (App, Container, Database, Router, Validator, Session, Response, Authenticator, exceptions, helpers)
- Core/ — core framework classes (App, Container, Database, Router, Validator, Session, Response, Authenticator, exceptions, helpers, etc...)
- Core/App.php
- Core/Container.php
- Core/Database.php
- Core/FileStorage.php - a thin facade / helper that delegates to the configured driver.
- Core/FileStorageDriver.php - the driver interface (contract) every storage adapter must implement.
- Core/LocalStorage.php - local filesystem implementation of the driver.
- Core/S3Storage.php - an Amazon S3 implementation that uses `aws/aws-sdk-php`.
- Core/Storage.php - an optional factory that decides which driver to instantiate based on `FILE_STORAGE_DRIVER` value.
- Core/Router.php
- Core/Validator.php
- Core/Session.php
Expand All @@ -35,17 +42,17 @@ Top-level files and folders in the repository:
- Core/functions.php
- Core/Middleware/ (middleware hooks)
- Http/ — HTTP layer (request/response helpers, middleware, adapters)
- bootstrap.php bootstraps the Container and binds Database into it
- bootstrap.php - bootstraps the Container and binds Database into it
- composer.json
- config.php returns an array with the configuration for db and api keys (do not push into prod)
- public/ document root / front controller (not populated in the repo root)
- routes.php application route declarations
- tests/ test cases
- views/ templates
- config.php - returns an array with the configuration for db and api keys (do not push into prod)
- public/ - document root / front controller (not populated in the repo root)
- routes.php - application route declarations
- tests/ - test cases
- views/ - templates

## Requirements

- PHP 8.0+ (check composer.json for any platform constraints)
- PHP 8.4+ (check composer.json for any platform constraints)
- Composer for autoloading and installing dependencies
- Typical PHP extensions (json, mbstring, etc.)

Expand Down Expand Up @@ -123,6 +130,39 @@ Adjust usage based on the public methods exposed by Core\Database (see Core/Data

Place route definitions in routes.php and ensure your front controller requires it after bootstrapping.

## Environment variables

This project now uses `vlucas/phpdotenv` to manage environment variables. Add a `.env` file to the project root to configure secrets and environment-specific settings.

Here's `.env.local` file which is a general template that after creating your project, do:

```bash
cp .env.local .env
```

To make a second clone of it, then change it to your projects needs:

```.env
APP_ENV=development
APP_DEBUG=true

FILE_STORAGE_DRIVER=local
FILE_STORAGE_LOCAL_PATH=storage/

DATABASE_DRIVER=sqlite
# DATABASE_HOST=localhost
# DATABASE_NAME=myapp
# DATABASE_CHARSET=utf8mb4
# DATABASE_USERNAME=root
# DATABASE_PASSWORD=

# AWS_ACCESS_KEY_ID=
# AWS_SECRET_ACCESS_KEY=
# AWS_DEFAULT_REGION=
# AWS_DEFAULT_ENDPOINT=
# AWS_BUCKET=
```

## Configuration

config.php should return an associative array. bootstrap.php expects the following structure at minimum:
Expand All @@ -139,6 +179,33 @@ return [

Edit config.php to match the connection options required by Core\Database.

## File Storage API

The API is intentionally simple. Common methods available on the driver and facade:

- put(string $path, string $contents): bool
- get(string $path): ?string

Snippet of usage:

```php
// File Upload Snippet using the newly added file storage api.
\Core\Storage::resolve()->put('file.txt', 'Hello, World!');
```

To get files:

```php
$savePath = __DIR__ . '/storage/hello-s3.txt';

if (!is_dir(dirname($savePath))) {
mkdir(dirname($savePath), 0777, true);
}

file_put_contents($savePath, $s3File);
echo 'Done!';
```

## Testing

write you tests in /tests/Unit for single unit tests, or in /tests/Feature for feature tests, then run tests with:
Expand Down
4 changes: 3 additions & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@
"description": "very simple and minimum Micro Framework built with PHP.",
"license": "MIT",
"require": {
"illuminate/collections": "^12.36"
"illuminate/collections": "^12.36",
"vlucas/phpdotenv": "^5.6",
"aws/aws-sdk-php": "^3.369"
},
"autoload": {
"psr-4": {
Expand Down
6 changes: 3 additions & 3 deletions config.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@

return [
'database' => [
'host' => 'localhost',
'dbname' => 'myapp',
'charset' => 'utf8mb4'
'host' => $_ENV['DATABASE_HOST'],
'dbname' => $_ENV['DATABASE_NAME'],
'charset' => $_ENV['DATABASE_CHARSET']
]
];
11 changes: 11 additions & 0 deletions public/index.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,17 @@
const BASE_PATH = __DIR__ . '/../';
require BASE_PATH . 'vendor/autoload.php';

use Dotenv\Dotenv;

$dotenv = Dotenv::createImmutable(__DIR__);
$dotenv->load();

//* File Upload Snippet using the newly added file storage api.
/**
* \Core\Storage::resolve()->put('hello.txt', 'Hello, World!');
* echo 'Done!';
*/

session_start();

require BASE_PATH . 'Core/functions.php';
Expand Down