diff --git a/.env.local b/.env.local new file mode 100644 index 0000000..a203d8b --- /dev/null +++ b/.env.local @@ -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= diff --git a/Core/Container.php b/Core/Container.php index f8c9114..8bae0e6 100755 --- a/Core/Container.php +++ b/Core/Container.php @@ -2,6 +2,9 @@ namespace Core; +use function array_key_exists; +use function call_user_func; + class Container { public function bind(string $key, callable $resolver): void @@ -9,7 +12,7 @@ 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}'"); diff --git a/Core/FileStorage.php b/Core/FileStorage.php new file mode 100644 index 0000000..2f2a747 --- /dev/null +++ b/Core/FileStorage.php @@ -0,0 +1,9 @@ +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; +} diff --git a/Core/S3Storage.php b/Core/S3Storage.php new file mode 100644 index 0000000..555a64f --- /dev/null +++ b/Core/S3Storage.php @@ -0,0 +1,41 @@ +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()}"); + } + } +} diff --git a/Core/Storage.php b/Core/Storage.php new file mode 100644 index 0000000..3490da3 --- /dev/null +++ b/Core/Storage.php @@ -0,0 +1,35 @@ + '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.'); + } +} diff --git a/README.md b/README.md index 5f19717..34dbad3 100644 --- a/README.md +++ b/README.md @@ -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) @@ -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 @@ -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.) @@ -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: @@ -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: diff --git a/composer.json b/composer.json index 7659cfa..5ce4bd4 100755 --- a/composer.json +++ b/composer.json @@ -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": { diff --git a/config.php b/config.php index 4c7c99d..8ff6992 100755 --- a/config.php +++ b/config.php @@ -2,8 +2,8 @@ return [ 'database' => [ - 'host' => 'localhost', - 'dbname' => 'myapp', - 'charset' => 'utf8mb4' + 'host' => $_ENV['DATABASE_HOST'], + 'dbname' => $_ENV['DATABASE_NAME'], + 'charset' => $_ENV['DATABASE_CHARSET'] ] ]; diff --git a/public/index.php b/public/index.php index 2dd930f..b94f7b0 100755 --- a/public/index.php +++ b/public/index.php @@ -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';