Skip to content

Contribute

Set up the Brezel skeleton

Install a Brezel instance which serves as a “container” and test environment for your contributions. You will later link your instance against installations of API and SPA.

Clone the Brezel base repository:

Terminal window
git clone git@gitlab.kiwis-and-brownies.de:kibro/brezel/brezel.git

Then, follow the Getting started guide to set up a Brezel instance or use an existing instance.

API installation

1. Clone Brezel API

Terminal window
git clone git@gitlab.kiwis-and-brownies.de:kibro/brezel/api.git

2. Install or update the Brezel components

Go the directory you just cloned:

Terminal window
cd api

Install composer packages:

Terminal window
composer install

3. Configure your Brezel environment

Copy the .env.example file to .env and edit the .env file.

For a full list of settable environment variables, consult the environment variable reference. To get started, make sure to set the following variables:

Terminal window
APP_URL="http://brezel.test"

4. Linking

There are two ways to link API. The easy way is using the composer package composer-link and is described here.

  1. Install the package globally: composer global require sandersander/composer-link
  2. Run this in the root folder of the brezel instance where you want to link your local api to: composer link <path/to/your/local/api/repo>

5. Unlinking

Just run composer unlink <path/to/your/local/api/repo> in the same directory you ran link in before.

SPA installation

1. Clone Brezel SPA

Terminal window
git clone git@gitlab.kiwis-and-brownies.de:kibro/brezel/spa.git

2. Install NPM packages

Go the directory you just cloned:

Terminal window
cd spa

Install npm packages:

Terminal window
npm install

3. Linking

First, register your SPA repository with your local npm.

Terminal window
cd /path/to/spa
npm link

Then, change the entrypoint of @kibro/brezel-spa. In the SPA repository, edit the package.json file and change the import value of . to ./lib/main.ts:

package.json
{
"exports": {
".": {
"import": "./lib/main.ts",
"import": "./dist-lib/brezel-spa.js",
"require": "./dist-lib/brezel-spa.umd.cjs"
},
"./src/*": "./src/*",
"./dist/*": "./dist-lib/*",
"./dist-lib/*": "./dist-lib/*",
"./vite.config": "./vite.config.js"
}
}

Then, in your Brezel instance, link the SPA package to your SPA repository:

Terminal window
cd /path/to/brezel
npm link @kibro/brezel-spa

Then, start your dev server normally.

Terminal window
npm run dev

Writing Tests

When adding new features to brezel/api, it is important that you test your work.

We utilize Pest for that, but raw PHPUnit tests are also fine. As long as you test at all.

You can take a look at the existing tests to find examples on how to setup a testable brezel instance with module etc. You will also find helpers for making requests and validating Brezel Properties.

Typing your tests

Because our available Entity type is generic for all entities, but we might want to access our custom defined fields in the tests, you need to properly type your objects or else your IDE and our Code analysis will yell at you.

Via Classes

The best way to do this is to create classes that extend the Entity class and add your custom fields to them. This way, you can properly typehint your objects and access your custom fields.

An example to put at the top of your file would look like this:

class IngredientListEntry extends Entity
{
public int $index;
public string $name;
public Entity $ingredient;
}
class Pastry extends Entity
{
public string $name;
public float $price;
/** @var Collection<IngredientListEntry> */
public Collection $ingredients;
}

This way you have an entity “Pastry” that hat a list of ingredients, which are of type “IngredientListEntry”. Each of those list entries has a select field ingredient which is simply of type Entity. If you would have a special type for this “ingredient” entity, you would repeat the process, create a class for it and reference it in the IngredientListEntry.

Via @phpstan-type annotations

If you do not want to create Classes, you can use virtual phpstan types. However, this is more complicated and will cause your custom added properties to be not writable! But it will keep your PHP source “clean” and not confuse you with custom classes.

To do that, you should use the @phpstan-type <Type> annotation at the top of your test class. (Idk how to do it in Pest function style tests…) This will enable “temporary” types that will satisfy our typechecking and will (depending on your IDE support) provide autocomplete and typehinting.

An example, including explanations would look like this:

/**
* @phpstan-type IngredientListFields object{index: int, name: string, ingredient: Entity}
* @phpstan-type IngredientListEntry ListEntity&IngredientListFields
* @phpstan-type PastryFields object{name: string, price: float, ingredients: Collection<IngredientListEntry>}
* @phpstan-type Pastry Entity&PastryFields
*/
class SomeTest extends TestCase
{
public function testSomething() {
// Here we initialize our test system based on bakery definitions.
// For this example, assume that this loads a system with a pastry module and an ingredient module.
// A "Pastry" Entity has a name, price, and a list of ingredients.
// An "Ingredient" Entity has a name and a price.
$planner = new BakeryPlanner();
$add = $planner->planFromFileSystem($this->getPath('pastrySystem'));
$add->apply();
// Now, when we get all of our Entities from the pastry Module, we can typehint it as a Collection of `Pastry`
$module = Module::byIdentifier('pastries');
/** @var Collection<Pastry> $entities */
$entities = $module->getAllResources();
// Here we pick out some Entity by name from all of our Entitites.
// We can see here, that we can typehint the result as a `Pastry` Entity. It is important that we only HINT when
// using those types, as they doe not really exist and cannot be used in PHP directly. This is also the reason we
// cannot type $entity (at least like `Entity $entity`, we could split the line and add an @var annotation). It's
// type should be inferred by the Collection typing done above.
//
// If we now take a look at the top of the file, we can see how the Type we used is defined.
// It is an "Intersection Type" between the Entity and the fields defined in the module definition, meaning that
// both the properties of Entity and the Object of fields it is intersecting are present on the final type.
// For simplicity sake, those fields are also their own type, you could just put them in one if you want.
// The same principle of intersection typing applies to a List field, which is also shown here.
// You define how an entry into the list should look like (an intersection between ListEntity and the custom fields)
// and use that as the type for a collection on your main Entity.
// Select fields are just fields with the type Entity (or for multiselects, they are type hinted collections again)
/** @var Pastry $pretzel */
$pretzel = $entities->first(fn($entity) => $entity->name === 'Pretzel');
// In this case we use the ingredient list type directly.
// Important to note here is that we again, don't need to typehint `$item->ingredient` here as it should already
// be properly inferred by the typehints done before.
// If for some reason those do not work, you can always split the line and add a `@var IngredientListEntry $item`
// annotation. But this should be your last resort and remember, only use annotations! These are not "real" types.
/** @var IngredientListEntry $salt */
$salt = $pretzel->ingredients->first(fn($item) => $item->ingredient->name === 'Salt');
self::assertNotNull($salt);
}
}