Skip to content

Learning project to figure out a simple CMS from the ground up

Notifications You must be signed in to change notification settings

Anpanator/SmolCms

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

58 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

SmolCms

This is a learning project for me using PHP8. Since I haven’t decided on a license yet, I don’t accept contributions at the moment. If you want to use any code you’ll have to ask for permission for the same reason.

Early startup configuration

If you need to run bootstrapping code very early in the application startup, you can utilise the ApplicationStartupHandler service.

In its service definition, it will take any number of StartupAction parameters, which will be run, in order, when the application starts.

new Service(
    identifier: ApplicationStartupHandler::class,
    parameters: [
        MyStatupAction::class,
        FancyStartupAction::class,
    ]
)

Configuring services

Check the ServiceConfiguration class. For now, it’s just hard-coded in the constructor. Configuration of a new service is only necessary if:

  • You need multiple instances

  • You have scalar/array type dependencies

Everything else will be autowired.

If you manually configure a service, and it depends on another service that does not have a service configuration, you can set the fully qualified classname of the service instead (e.g. MyService::class). If you have a service definition, you can set the identifier of the service you want to inject. The parameters array needs to have the same order as the constructor parameters.

Variadic parameters in the form of MyService …​$services are supported for services with a configuration, but not for autowiring.

The class parameter can be omitted if the identifier is the fully qualified class name.

Routing

You can define routes in the RoutingConfiguration class, similar to the service configuration.

Controllers are treated like services. So in the simplest case, you use the fully qualified classname for the controller, e.g. MyController::class. Alternatively, you can use the controllers service identifier.

Routes can contain path parameters in the form of /some/path/with/{parameter}. Every path parameter must exist in the handler function with the same name (either untyped, mixed or type string, which is recommended).

A handler must also have a $request parameter. The order of all these parameters is irrelevant. If the handler configuration is omitted, the handler name is assumed to be HTTP Method + 'Action', e.g. getAction, postAction, etc.

So with the example path above and a POST request, you’d need a handler method like this:

class SomeController {
    public function postAction(Request $request, string $parameter): Response
    {
        // ...
    }
}

Templates

If you want to generate responses from templates, you can do so by using the TemplateService in the controller action like this:

public function getAction(Request $request): Response
{
        return $this->templateService->generateResponse(
            new ArticleTemplateConfig(
                language: "en",
                pageTitle: "Nice Boat",
                articleContent: "Fancy ass content"
            )
        );
}

Your actual template structure is composed through a configuration class implementing the TemplateConfig interface, e.g.:

readonly class ArticleTemplateConfig implements TemplateConfig
{

    public function __construct(
        private string $language,
        private string $pageTitle,
        private string $articleContent
    )
    {
    }

    public function getConfig(): array
    {
        return [
            HtmlTemplate::class => [
                HeadComponent::class => [$this->pageTitle],
                ArticleComponent::class => [$this->articleContent],
                $this->language
            ]
        ];
    }
}

This will ensure that autocompletion and proper typing will be available.

Every template and component needs to implement the Template interface.

readonly class HeadComponent implements Template
{

    public function __construct(
        private string $pageTitle
    )
    {
    }

    public function render(): string
    {
        return <<<HTML
        <head>
            <title>$this->pageTitle</title>
            <link type="text/css" rel="stylesheet" href="/public/css/main.css">
        </head>
        HTML;
    }
}

Validation

For validation on object properties, the generic Validator service can be used. The object to validate is simply passed into its validate() method. For validation to work, you will need to add attributes implementing the PropertyValidationAttribute interface on the object properties (multiple are supported). If you want to have nested object validation, simply add the ValidateObject attribute to the property.

Usage:

class Foo {
    public function __construct(
    #[ValidateRange(min: 10, max: 100)]
    private float $floatVal,
    #[ValidateRange(min: -10, max: 0)]
    private int $intVal,
    #[ValidateAllowList(['ALLOWED', 1, true])]
    private mixed $mixedAllow,
    #[ValidateDenyList(['DENIED', 'ALSO_DENIED'])]
    private string $stringDeny,
) {
}
}

$foo = new Foo(
    floatVal: 15.0,
    intVal: -1,
    mixedAllow: 'ALLOWED',
    stringDeny: 'Not denied'
);
$result = $validator->validate($foo);
var_dump($result);

To support a new validation attribute, you only need to create it and have it implement the PropertyValidationAttribute interface. The validator will then use it automatically.

Testing

For ease of testing, the Mock attribute, SimpleTestCase and FunctionalTestCase classes have been introduced.

The setUp() method will automatically put an unconfigured test double into the property you use the Mock attribute on.

Usage:

class ServiceBuilderTest extends SimpleTestCase
{
    private ServiceBuilder $serviceBuilder;
    #[Mock(ServiceConfiguration::class)]
    private ServiceConfiguration|MockObject $serviceConfiguration;
    #[Mock(ServiceRegistry::class)]
    private ServiceRegistry|MockObject $serviceRegistry;
//...
}

Note: The property type hinting is not necessary and just used for convenient auto completion.

Additionally, the FunctionalTestCase class will provide an ApplicationCore that will allow you to simulate requests even without a web server. This is useful when you want to create automated tests for JSON api endpoints for instance.

About

Learning project to figure out a simple CMS from the ground up

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published