Architecture

We use certain conventions within Snipe-IT that are not necessarily Laravel specific, but specifically to Snipe-IT. Understanding this will help you navigate the codebase more easily.

Models, Searchability, Relationships and Query Scopes

Each "first class" object (assets, people, etc) has a model file in the app/Models directory. They will generally start with model-level validation rules and a standard $fillable protected array. Model-level validation is not a Laravel standard, but we use dwightwatson/validating, so you'll see a $rules definition that defines the shape of the model itself. This means that if these rules are not met, the object itself cannot be saved.

Changing these can cause weird problems in ways you might not expect, so make sure you run tests before submitting a PR.

Searchability

Snipe-IT uses an abstraction for searching across tables that makes it very easy (sometimes dangerously so, if you're not checking the number of queries you're firing off) to search across tables based on a defined set of parameters. For example, if we take a look at the Asset model:

use Searchable;

    /**
     * The attributes that should be included when searching the model.
     * 
     * @var array
     */
    protected $searchableAttributes = [
      'name', 
      'asset_tag', 
      'serial', 
      'order_number', 
      'purchase_cost', 
      'notes', 
      'created_at',
      'updated_at',      
      'purchase_date', 
      'expected_checkin', 
      'next_audit_date', 
      'last_audit_date',
      'last_checkin',
      'last_checkout',
      'asset_eol_date',
    ];

    /**
     * The relations and their attributes that should be included when searching the model.
     * 
     * @var array
     */
    protected $searchableRelations = [
        'assetstatus'        => ['name'],
        'supplier'           => ['name'],
        'company'            => ['name'],
        'defaultLoc'         => ['name'],
        'location'           => ['name'],
        'model'              => ['name', 'model_number', 'eol'],
        'model.category'     => ['name'],
        'model.manufacturer' => ['name'],
    ];

The $searchableAttributes array defines what fields on that specific model should be searchable.

The $searchableRelations array is where a lot of the heavy lifting happens. The key in the array is the Eloquent relationship method name (defined further down in the model file) and the value is an array of what fields within that relationship we should search on.

For example:

protected $searchableRelations = [
        'assetstatus'        => ['name'],
        'supplier'           => ['name'],
        'company'            => ['name'],
        'defaultLoc'         => ['name'],
        'location'           => ['name'],
        'model'              => ['name', 'model_number', 'eol'],
        'model.category'     => ['name'],
        'model.manufacturer' => ['name'],
    ];

This is asking Eloquent to look at the assetstatus() model method, and saying that we want to search only on the name value of that status.

In the case of the model() relationship, you can see that we want to search on the name, model_number, and eol fields.

In these $searchableRelations definitions, the key must always map to a relationship model method name, and the corresponding array values should always map to fields that exist on that related model.

Presenters

In the app/Presenters directory, you will find several presenter files. These presenters are primarily used to configure the bootstrap tables columns so that they are consistent throughout the system.

public static function dataTableLayout()
    {
        $layout = [
            [
                'field' => 'checkbox',
                'checkbox' => true,
            ], (etc...)
     }

These fields correspond directly with the data- attributes within the bootstrap table javascript library.

In some cases, you'll also find some more generalized presenters, such as $user->fullName(), since we do not have a fullname field on the user's table, but there are times we don't want to junk up the blades with if/else logic on whether or not there is a last name value.

If you are adding a new presenter method, please always add them at the bottom of the file, and be sure to include a comment block to describe what it does.

Transformers

Snipe-IT uses transformers to define the shape of the API. You can find these in the app/Http/Transformers directory.

Every transformer will start with something like this:

public function transformStatuslabels(Collection $statuslabels, $total)
    {
        $array = [];
        foreach ($statuslabels as $statuslabel) {
            $array[] = self::transformStatuslabel($statuslabel);
        }

        return (new DatatablesTransformer)->transformDatatables($array, $total);
    }

(in this case, we're "transforming" the status label API results).

That transformStatuslabels() method handles one or many status label results. (Don't worry about the DatatablesTransformer() - you'll never need to change that.)

And then the next method handles the shape of the single object, in this case, transformStatuslabel():

public function transformStatuslabel(Statuslabel $statuslabel)
    {
        $array = [
            'id' => (int) $statuslabel->id,
            'name' => e($statuslabel->name),
            'type' => $statuslabel->getStatuslabelType(),
            'color' => ($statuslabel->color) ? e($statuslabel->color) : null,
            'show_in_nav' => ($statuslabel->show_in_nav == '1') ? true : false,
            'default_label' => ($statuslabel->default_label == '1') ? true : false,
            'assets_count' => (int) $statuslabel->assets_count,
            'notes' => e($statuslabel->notes),
            'created_at' => Helper::getFormattedDateObject($statuslabel->created_at, 'datetime'),
            'updated_at' => Helper::getFormattedDateObject($statuslabel->updated_at, 'datetime'),
        ];

        $permissions_array['available_actions'] = [
            'update' => Gate::allows('update', Statuslabel::class) ? true : false,
            'delete' => (Gate::allows('delete', Statuslabel::class) && ($statuslabel->assets_count == 0)) ? true : false,
        ];
        $array += $permissions_array;

        return $array;
    }

This allows us to standardize API results across the boards, for people using the API directly via integrations, and also via Snipe-IT itself, whether there are 1000 results or only one (or none.)

When we want to invoke this in the API controllers (in this case, app/Http/Controllers/Api/StatusLabelsController.php), we invoke:

return (new StatuslabelsTransformer)->transformStatuslabels($statuslabels, $total);

This means that regardless of what question we're asking of the API, the shape always remains the same. This makes it easier for developers and IT admins to rely on the consistency of the JSON payload for their own integrations.

🚧

Making a change to the transformers can and often WILL break third-party integrations. It will usually require a major release.

Query Scopes

We utilize Laravel query scopes heavily throughout the system. At the end of every model file, you'll see a comment block like this:

    /**
    * -----------------------------------------------
    * BEGIN QUERY SCOPES
    * -----------------------------------------------
    **/

If you are adding a new query scope, please make sure to add it to the bottom of the file. All query scopes start with scope, and then camel-cased description of what the scope does.

For example, this query scope allows us to search only on assets that are not marked as "archived"

public function scopeNotArchived($query)
    {
        return $query->whereHas('assetstatus', function ($query) {
            $query->where('archived', '=', 0);
        });
    }

and when we want to use this scope, we would invoke it as $assets->NotArchived().