PDS Skeleton by Example: A Standard for File and Folder Structure

0
122

Looking at the Packagist registry, we can see that most packages follow a pattern, with some small changes to fit their needs, while others have a weird folder structure that takes time to get your head around.

This problem has been solved in different ways by different people. Some frameworks have their own recommendations on how to structure your classes and assets, while others have a skeleton that you can use as a starting point. In this article, we’re going to explore the php-pds/skeleton and use it to build a small package as a demonstration.

Note: PDS stands for Package Development Standards.

A house plan top down view, illustration, indicating wireframe for future project

Ending Soon: Get Every SitePoint Ebook and Course for FREE!

87 Ebooks, 70 Courses and 300+ Tutorials all yours for FREE with ANY web hosting plan from SiteGround!

Claim This Deal!

What We’re Building

The idea is to have a way to map FAQ pages (or any other page) to exceptions thrown by our Laravel application. This will serve as a starting point for our users to work out what went wrong.

You can find the final code on GitHub. Feel free to send pull requests to improve it or suggest fixes! (If your Git is rusty, we have a good premium course for that)

PDS Skeleton

The PDS skeleton recognizes that there are already some widespread practices in use by developers who publish on Packagist. After some research using Packagist’s API to explore common folder structures, the author came up with an interesting summary.

When we publish something publicly, it must adhere to some common rules like being tested, documented, formatted, etc. The next step is to have a common folder structure to help developers and contributors understand the code flow.

Surely you’ve heard of the PHP league before! If you want to contribute a package to the league, you must be using their skeleton along with some other rules to be followed. I think having the same thing for the whole community would help improve the readability and consistency of all publicly released packages.

Building the Package

Since this is a Laravel package, I will be following this workflow. Go ahead and download the PDS skeleton package.

cd vendor
mkdir whyounes # this is my author namespace
curl -LOk https://github.com/php-pds/skeleton/archive/1.0.0.zip # Download file
unzip 1.0.0.zip
mv 1.0.0 laravel-faq-exceptions
rm 1.0.0.zip

We need to update our composer.json file so that it can be discovered by Composer when loading dependencies. Let’s keep it simple for now!

{
    "name": "Whyounes/laravel-faq-exceptions",
    "type": "library",
    "description": "Laravel package for mapping exception FAQ pages.",
    "homepage": "https://github.com/Whyounes/laravel-faq-exceptions",
    "license": "MIT",
    "require": {
        "laravel/framework": "~5.3"
    },
    "require-dev": {
        "pds/skeleton": "~1.0"
    },
    "autoload": {
        "psr-4": {
            "Whyounes\\FaqException\\": "src/"
        },
        "classmap": [
            "tests"
        ]
    },
    "autoload-dev": {},
    "bin": []
}

You may have noticed that the require-dev section contains the pds/skeleton package. This has two benefits.

  1. It helps track who’s using the skeleton, and developers can check it to see the naming rules.
  2. It includes the command line helper to generate and validate our skeleton.

Now that we have a starting point, we can init the repo and add our Git remote.

git init
git add .
git commit -m "first commit"
git remote add origin [email protected]:Whyounes/laravel-faq-exceptions.git
git push -u origin master

Naturally, replace the remote URL with your own repo’s URL.

The skeleton contains some classes by default as a demonstration, so go head and clean the directories.

Alternative Option

There is another way to generate the skeleton without downloading the zip file and unzipping it.

composer require --dev pds/skeleton
./vendor/bin/pds-skeleton generate

This will generate all missing files and folders.

Config

Usually, a package has some configuration files to rely on when deciding what to do. The PDS skeleton has a directory for this called config.

If the package provides a root-level directory for configuration files, it MUST be named config/.
This publication does not otherwise define the structure and contents of the directory.

We only have one config file for now. We can put it in there, and we will refer to it in our service provider (more about this later).

Resources

Our package needs to have some DB access to be able to map exceptions to FAQ pages. This means we need to create a migration for this purpose.

PDS Skeleton has a resources directory that is described as:

If the package provides a root-level directory for other resource files, it MUST be named resources/.
This publication does not otherwise define the structure and contents of the directory.

The rules don’t define how the underlying structure should look. Currently, we’ll use it to store our migrations and views. It can also be used to store seeders, language files, etc.

// resources/migrations/2014_10_12_000000_create_faq_table.php

class CreateFaqTable extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('whyounes_faq', function (Blueprint $table) {
            $table->increments('id');
            $table->text('exception');
            $table->string('codes');
            $table->text('url');
        });
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::dropIfExists('whyounes_faq');
    }
}

We’re also going to need a view file to display the thrown exception:

// resources/views/faq.blade.php

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1">

    <title>Laravel</title>

    <!-- Fonts -->
    <link href="https://fonts.googleapis.com/css?family=Raleway:100,600" rel="stylesheet" type="text/css">

    <!-- Styles -->
    <style>
        html, body {
            background-color: #fff;
            color: #636b6f;
            font-family: 'Raleway', sans-serif;
            font-weight: 100;
            height: 100vh;
            margin: 0;
        }
    </style>
</head>
<body>
<div class="flex-center position-ref full-height">
    <div class="content">
        <div class="links">
            An error has occured: {{ $exception->getMessage() }}
            @if(!is_null($faq))
                <br>You can get more details about the problem <a href="{{ $faq->url }}">here</a>.
            @endif
        </div>
    </div>
</div>
</body>
</html>

Source Files

Ok, now we get to the actual package logic. The skeleton has a src directory that is described in the documentation as:

If the package provides a root-level directory for PHP source code files, it MUST be named src/.
This publication does not otherwise define the structure and contents of the directory.

The src directory is obviously the place where we store our package’s source code, and, as before, the specification doesn’t describe the underlying structure.

directory structure

The actual source code is in the repo, but let’s briefly discuss these classes.

  • src/Models/Faq.php: This is the model class for interacting with our DB table. We group models under the same directory here.
  • src/Providers/FaqProvider.php: The provider will hold our package’s service providers. We use it to publish assets (like views, migrations, config), register IoC bindings, routes, etc.
  • src/Repositories/FaqRepository.php: This is a repository for interacting with our Eloquent models. I prefer to keep them separate from the models directory.

We also have some classes at the root of the src directory. This is not violating any rules of the skeleton. In fact, it makes sense for some classes to be in the root of the namespace.

How Does the Package Work?

The user will use one of the renderers depending on the application type. The WebRenderer class will render the exception using the faq.blade.php template. The ApiRenderer will return a JSON response containing the exception details.

The typical usage will look like this:

// app/Exceptions/Handler.php

class Handler extends ExceptionHandler
{
    // ...
    public function render($request, Exception $exception)
    {
        if ($request->expectsJson()) {
            $renderer = App::make(ApiRenderer::class);
        } else {
            $renderer = App::make(WebRenderer::class);
        }

        return $renderer->render($exception);
    }
    // ...
}

Tests

The skeleton also contains a tests folder, which we’ll use for our package’s tests.

If the package provides a root-level directory for test files, it MUST be named tests/.
This publication does not otherwise define the structure and contents of the directory.

The classes we have to test here are the WebRenderer, ApiRenderer and Models/Faq. Even though the specification doesn’t specify the underlying structure of the tests folder, I advise you to make it identical to your src folder, which makes searching for tests and locating them easy and predictable.

I think that this should be RECOMMENDED by the skeleton spec as a best practice. Most packages do this and it seems logical.

You can check out the repo on GitHub to see the actual test code.

Public

Our package doesn’t have any public assets that need to be accessed by the user. But in case you have some in yours, make sure to put them here.

If the package provides a root-level directory for web server files, it MUST be named public/.
This publication does not otherwise define the structure and contents of the directory.

The confusing part here is whether to use the /resources directory or the /public one. The answer here is “it depends”. If we’re building a package for a framework like Laravel, we have the right to specify assets to be published by the framework into the framework’s own /public folder. However, if we’re using the skeleton for a full application, we can use the /public folder as the web root.

Bin

Some packages provide executable files to accomplish a task. In such cases, the /bin directory might be useful. For example, packages like PHP Code Sniffer are ideal candidates for this (but the developer in this case chose to call the folder scripts ¯\_(ツ)_/¯ )

Documentation

Documentation is an essential part of every application/package. It is the go to place for software consumers and developers to get an idea of how things work, and what they’re doing wrong.

If the package provides a root-level directory for documentation files, it MUST be named docs/.
This publication does not otherwise define the structure and contents of the directory.

PDS skeleton provides a separate directory for the documentation that we can structure however we want. The best approach, in my opinion, is to separate it into topics and have them in separate directories. In the case of an API, we can separate it into endpoints.

README

The README file is very important, but not officially a part of /docs. It gives users an overview of how to install and integrate your code, along with some other general details. Our README file looks like this.

You can see that the file only contains a few details about the package; it’s just an overview, no need to make it too long. The docs directory should contain the rest of the in-depth information.

License

Not many of us care about the package license, but it’s still an important step before publishing anything. Someone may want to use the package in unexpected ways, and they need to know if this action is permitted or not.

This website has a really nice overview of the different available licenses. For our package, we will use the MIT license.

Contributing

Another part that is often ignored by developers is the contribution guideline. The application must have a clean process for other developers to contribute features and fixes.

Note: GitHub automatically links to this file when someone is making a new pull request.

We don’t have to create one from scratch. We can chose one that suits our needs and copy it. The file should include:

  • how to submit bug reports and fixes.
  • how to submit security fixes.
  • which coding style is used.

Other things may be included, but this should be enough to get contributors started.

Thanks for choosing to contribute to this package.

## Bug Fixes

Bug fixes for a specific release should be sent to the branch containing the bug. You can also submit a failing test showing where the bug occurs.

## Security Fixes

Security fixes may be sent as a PR or via email to [email protected]

## Coding Standard

This package follows the PSR-2 coding standard and the PSR-4 autoloading standard.

Changelog

The changelog file is used to track changes and bug fixes between releases. It’s easier to look at a file than to scroll over Git commits to see what happened. An example could be something like this:

# Changelog

## 1.0.37 - 2017-03-22

### Fixed

* Space escaping for Pure-FTPd in the FTP adapter.

## 1.0.36 - 2017-03-18

### Fixed

* Ensure an FTP connection is still a resource before closing it.
* Made return values of some internal adapters consistent.
* Made 0 a valid FTP username.
* Docblock class reference fixes.
* Created a more specific exception for when a mount manage is not found (with BC).

## 1.0.35 - 2017-02-09

### Fixed 

* Creating a directory in FTP checked whether a directory already existed, the check was not strict enough.

## 1.0.34 - 2017-01-30

### Fixed

* Account for a Finfo buffer error which causes an array to string conversion.
* Simplified path handling for Windows.

// ...

The above snippet is taken from the PHPLeague’s flysystem package.

Validation

We can use the validate command to see if our structure respects the PDS rules.

./vendor/bin/pds-skeleton validate

Validate skeleton

Conclusion

Using a documented skeleton will not just help you organize your files, but it also helps others predict where things are or should be. The PDS skeleton is not the only one that exists out there, but others like the PHPLeague skeleton have already been adopted by a large number of developers, and it’s PDS compliant by default. This means that it respects the same rules and is considered valid when being checked against the validator.

Do you use a skeleton when creating a new application or package? Do you think we should have a wide-reaching standard for this too? Let us know in the comments!

Suggest

The Complete PHP Bootcamp Course With Video Sharing Project

PHP OOP: Object Oriented Programming for beginners + Project

Object Oriented Programming (OOP) in PHP – Build An OOP Site

Source viva: https://www.sitepoint.com/pds-skeleton-by-example-a-standard-for-file-and-folder-structure/

LEAVE A REPLY

Please enter your comment!
Please enter your name here