fr8train / skeleton-ms
Skeleton MicroService powered by Slim4 Framework.
Package info
bitbucket.org/collettedevelopmentgroup/skeletonms
Type:project
pkg:composer/fr8train/skeleton-ms
Requires
- php: >=8.4
- ext-http: *
- ext-json: *
- ext-pdo: *
- guzzlehttp/guzzle: ^7.10
- monolog/monolog: ^3.9
- nesbot/carbon: ^3.11
- php-di/php-di: ^7.1
- ramsey/uuid: ^4.9
- slim/psr7: ^1.8
- slim/slim: ^4.15
- sorskod/db: ^1.1
- vlucas/phpdotenv: ^5.6
This package is auto-updated.
Last update: 2026-04-14 09:43:50 UTC
README
Skeleton MicroService powered by Slim4 Framework.
Installation
Use Composer to install SkeletonMS locally.
composer create-project fr8train/skeleton-ms [directory] [version]
If you want to include the git link to send updates to the repository, use:
composer create-project --keep-vcs fr8train/skeleton-ms [directory] [version]
Post-Installation
Please create a .env file at the root directory of the project.
If you would like to use the provided Database Singleton for MySQL, please add these to your .env file:
MYSQL_HOST=""
MYSQL_DATABASE=""
MYSQL_USERNAME=""
MYSQL_PASSWORD=""
Seriously, I just ran into this again. Without at least a blank .env file, you will receive a HTTP 500 error. A blank one is required for the project to run, and we recommend that you use it as the location to store any sensitive information such as passwords or keys. To retrieve any data stored here, use vlucas/phpdotenv.
If for some reason you get an error message indicating that a class in the code cannot be found, first try to reload the autoloaded classes through Composer.
composer dump-autoload
Useful Design Practices and Libraries
ramsey/uuid - PHP UUIDv7 libray
This was added to our project to increase security, as working with UUIDs continues to rise and leave predictable int unsigned IDs behind. We're using specifically his UUIDv7 object as v7 UUIDs are universally unique lexicographically-sortable identifiers meaning they're UUIDs that can be sorted and searched in databases in meaningful ways (see his documentation here). Here's the main link to his documentation.
Here is an example on how to use it with IDs set up in MySQL 8 using a binary(16) column.
/*
* FOR AN OBJECT USING RAMSEY'S UUIDv7 CLASS
* YOU CAN USE THE FOLLOWING TO GET THE BINARY ID
*/
$id = Ramsey\Uuid\Uuid::uuid7()->getBytes();
/*
* FOR A RAW BINARY ID, USE THE FOLLOWING TO GET THE UUID OBJECT
*/
$id = Ramsey\Uuid\Uuid::fromBytes($id);
We also updated the ObjectFactory::loadClass() method so that you can also auto detect byte strings and attempt to translate them back into UUIDs.
$lists = $this->db->execQueryString("SELECT * FROM lists")
->fetchAll(PDO::FETCH_ASSOC);
$lists = array_map(fn(array $el): ListClass => ObjectFactory::loadClass(ListClass::class, $el), $lists);
sorskod/db PDO wrapper
This is a great little library for making DB work a lot simpler to manage. Information and practices can be found at his Github Page or his similar Packgist. One thing that isn't implicit in his documentation to note, if you would like to bind your parameters for SQL Injection scrubbing please follow this example:
// IF YOU HAVE BOOLEAN VALUES, DON'T FORGET TO CAST THEM TO INTs IN PHP
// OR IT WILL THROW AN ERROR
$stmt = $this->db->execQueryString("select * from users")]);
$users = $stmt->fetchAll(PDO::FETCH_ASSOC);
Insertable
To ease the process of dealing with the UUIDs conversion prior to DB inserts, we also came up with the UuidInsertableInterface, which requires a toInsertable() method to be created on the class.
class Item implements UuidInsertableInterface
{
public UuidInterface $id;
public UuidInterface $list_id;
...
public function toInsertable(): array
{
return [
'id' => $this->id->getBytes(),
'list_id' => $this->list_id->getBytes(),
'name' => $this->name,
'created_at' => $this->created_at,
'completed_at' => $this->completed_at,
];
}
}
...
$this->db->insert("lists", $list->toInsertable());
...
Updatable
Updatables is a feature that allows for a property-by-property distinction of which fields should be selected for (and against) creating an array of key/values to pass to the update method from sorskod's DB. It also pairs with the UpdatableTrait.
class Item
{
use UpdatableTrait;
// DON'T WANT TO UPDATE THE ID
public UuidInterface $id;
// DO WANT TO UPDATE NAME AND IS_MINE
#[Updatable]
public string $name;
#[Required, Updatable]
public bool $is_mine;
#[Updatable(UpdatableTypes::PASSWORD)]
public ?string $password;
}
...
$this->db->update("items", $item->toUpdatable(), "id = ?", [
$item->id->getBytes()
]);
...
Updatables also now have types, in order to support hashing passwords for example on an update to a User. We also added synergetic logic between Updatable and Required if you set toUpdatables() to strict mode.
CastableTrait
The Castable Trait provides a quick assignment to your PHP custom objects or classes as long as you remember to include default values in the constructor of your custom object or class.
// CUSTOM CLASS DEFINITION
class ServiceResponse
{
use CastableTrait;
public int $http_code;
public string $message;
public mixed $payload;
public ?Throwable $error;
public function __construct(int $http_code = 200,
string $message = '',
mixed $payload = null,
?Throwable $error = null)
{
$this->http_code = $http_code;
$this->message = $message;
$this->payload = $payload;
$this->error = $error;
}
}
// ACTUAL CASTING
// BECAUSE OF DEFAULT VALUES ANY ONE OF THESE PROPERTIES CAN ACTUALLY BE SKIPPED IN DECLARATION
return ServiceResponse::cast([
'http_code' => 500,
'message' => 'Internal Server Error',
'payload' => [
'trace' => $exception->getTrace()
],
'error' => $exception // instanceof Exception, Throwable, or null
]);
Validation
We recently added both the Required attribute and the ValidatableTrait to our project.
The Required attribute is a simple way to ensure that a property has to have a truthy value (we're using empty() to determine this) when attempting to communicate with our API. Simply add it to a property to ensure that when the ->validate() call is made, we will determine whether or not it is 'empty'.
class Foo {
use ValidatableTrait;
#[Required]
public string $bar;
}
The ValidatableTrait provides the methodology for validating the incoming data as well as formulating the response in a single location.
public function test(Foo $foo): ServiceResponse {
$errors = $foo->validate();
if (!empty($errors)) {
// RETURNS A HTTP 422, MISSING REQUIRED PARAMETERS
return $foo->formatServiceResponse($errors);
}
return ServiceResponse::cast([
'message' => 'I made it past the validator!'
])
}
ServiceResponse Design
Implementing Services by always returning ServiceResponses can prove to make your code much easier to navigate and reduce bugs by following the design:
<?php
namespace services;
use models\HelloWorld;
use models\ServiceResponse;
class HelloWorldService
{
public static function hello(HelloWorld $world) : ServiceResponse
{
/*
* DO YOUR SERVICE LOGIC
*/
// ANY OF THESE PROPERTIES ON SERVICE RESPONSE
// CAN TECHNICALLY BE BLANK BECAUSE OF DEFAULT VALUES
if (isset($exception)) {
return ServiceResponse::cast([
'http_code' => 500,
'message' => 'Internal Server Error',
'payload' => [
'trace' => $exception->getTrace()
],
'exception' => $exception
]);
}
return ServiceResponse::cast([
'message' => $world->message,
'payload' => [
'Foo' => 'Bar'
]
]);
}
}
See how handling the ServiceResponse makes Controller handling much easier:
$serviceResponse = HelloWorldService::hello(ObjectFactory::loadClass(HelloWorld::class, $data));
return match ($serviceResponse->http_code) {
200 => $this->json($response, $serviceResponse),
default => $this->error($response, $this->log, $serviceResponse)
};
In addition, it also increases DRY concepts when dealing with nested objects. For example, with the REST URL POST https://mysite.com/foo/1/bar checking for the existence of foo with ID=1 becomes a snap:
public function create(Foo $foo, Bar $bar): ServiceResponse
{
// DOES FOO EXIST?
$serviceResponse = $this->fooService->fetch($foo);
// IF NOT, RETURN HTTP 404 WRAPPED IN A SERVICERESPONSE OBJECT
if ($serviceResponse->http_code !== 200) return $serviceResponse;
$errors = $bar->validate();
if (!empty($errors)) {
return $bar->formatServiceResponse($errors);
}
$this->db->insert("bar", $bar->toInsertable());
return ServiceResponse::cast([
'payload' => $bar
]);
}
Null-Instantiation Design Pattern
By design, the system is built around the concept of "you expect it, it will be there" style of coding. All models are considered open, with public properties as placeholders in order to remove any doubt as to what properties exist on any of the custom classes in our project. This helps to align database fields as well as expected API parameters. It is also key to our ObjectFactory::loadClass(), which uses Reflection to determine what data existing on the source has identical fields on the target to which it can map the values. A lot of words to say, you don't need to isset($foo->bar) before checking for the value on an object ever again.
Singleton Design Pattern
We added the Singleton Design Pattern into our project because it became apparent that you can easily create way too many Database connection objects. Why bother when you can just pass around one? Relying on the SingletonInterface, custom singletons are very straight-forward to set up. You are required to:
- implement the
Singleton Interface - instantiate your object statically
- create a private static instance of this object
- create a private constructor to force "static"-ness
- and finally, create a method to return the existing instance
class YourObjectSingleton implements SingletonInterface
{
private static ?YourObject $instance = null;
private function () {}
public static function instantiate(): object
{
// INCLUDE ANY PARAMETERS YOU MIGHT NEED
// TO INJECT INTO YOUR CONSTRUCTOR
return new YourObject();
}
public static function getInstance(): object
{
return self::$instance ??= self::instantiate();
}
}
...
use YourObjectSingleton as yos;
// WHERE FOO() EXISTS ON YourObject
$bar = yos::getInstance()->foo();
...
Contributing
Pull requests are welcome. For major changes, please open an issue first to discuss what you would like to change.
Please make sure to update tests as appropriate.