Modules
Create modules, understand their anatomy, register routes and assets, and extend Aero models and core behaviour.
In this guide
- Module anatomy
- Create a module
- Module routes
- Vue components
- Product custom fields
- Product custom prices
- Model settings
- Address forms
- Address form fields
- Models
- Product slug manufacturer
- Order item manufacturer
Anatomy of a custom module
└─ module
└─ database
└─ migrations
└─ public
└─ resources
└─ css
└─ js
└─ views
└─ admin
└─ store
└─ routes
└─ src
└─ Http
└─ Controllers
└─ Responses
└─ Steps
└─ Requests
└─ Models
ServiceProvider.php
.gitignore
composer.json
README.md
Database
The database folder is only required for purposes of migrating, seeding, or storing data in formats such as JSON.
Migrations
Migrations is a folder within the database directory structure used for storing migration files which allow for populating the database with missing tables, or updating them.
Running php artisan migrate in the root project directory after installing a module detects migrations from all modules installed on Aero, which have not yet been migrated, and migrates them into Aero.
Public
All files within the public directory of a module can be published, to then be accessed anywhere in Aero. Some modules might require the public folder published before it can be used.
This might include images or files used to instantiate a JavaScript file for the use of Vue or any other JS framework.
Resources
The resources are required by the module for rendering views and storing custom CSS and JS files. Separating views into sub-folders allows for more readability and clarity on where particular views are used.
For example, the admin and store sub-folders of resources/views can be used for separating the different routing the module has.
Routes
Modules can have custom routes which are then served to the rest of the app. These might serve as callback URLs for APIs in the module or rendering views.
Example:
<?php
use Aerocargo\Csv\Http\Controllers\CsvController;
use Illuminate\Support\Facades\Route;
Route::group(['prefix' => 'csv'], function ($route) {
$route->get('/', [CsvController::class, 'index'])->name('admin.csv.import');
$route->get('/mapping/{hash?}', [CsvController::class, 'mapping'])->name('admin.csv.mapping');
$route->get('/search', [CsvController::class, 'aeroFieldsSearch'])->name('admin.csv.search.aero');
$route->get('/search-csv', [CsvController::class, 'csvFieldsSearch'])->name('admin.csv.search.csv');
$route->get('/processing', [CsvController::class, 'processing'])->name('admin.csv.processing');
$route->post('/importing', [CsvController::class, 'importing'])->name('admin.csv.importing');
$route->post('/process', [CsvController::class, 'process'])->name('admin.csv.process');
$route->post('/import', [CsvController::class, 'import'])->name('admin.csv.process.import');
$route->post('/ping-import', [CsvController::class, 'pingImport'])->name('admin.csv.process.ping');
$route->post('/get-mapping/{id?}', [CsvController::class, 'getMappingData'])->name('admin.csv.get.mapping');
});
Source (src)
The source folder contains all of the backend serving the entire module, whether it is controllers, models, or traits.
Service Provider
The module Service Provider is used to instantiate the module, and usually contains configuration for the resources the module uses. This could range from routes, views to being able to display the module and its UI within the Modules section of Aero.
The scaffolded Service Provider has several functions that are hidden from view.
These include:
- setup()
- assetLinks()
- boot()
- seed()
- getSeeds()
setup()
Every scaffolded Service Provider will include the setup() function, which has to be configured to include all necessary resources.
Example:
public function setup()
{
AdminSlot::inject('catalog.products.index.header.buttons', function () {
return view('csv::buttons.import');
});
Router::addAdminRoutes(__DIR__.'/../routes/admin.php');
$this->loadMigrationsFrom(__DIR__.'/../database/migrations');
$this->loadViewsFrom(__DIR__.'/../resources/views', 'csv');
BulkAction::create(ExportProducts::class, ProductsResourceList::class)
->notRunnable()
->permissions('products.export')
->title('Export products');
}
assetLinks()
Asset links are used to publish the resources included in the [public folder](#public) of the module so that they can be used anywhere on the Aero platform.
Example:
public function assetLinks()
{
return [
'vendor/module-name' => __DIR__ . '/../public',
];
}
boot()
The boot() method is run in the background if the setup() method is present within the Service Provider. This is also the case for the booted() method.
seed()
With the seed() method, it is possible to add a seed to a collection of seeds for the module.
getSeeds()
Returns a unique collection of seeds that have been added to the module.
If we require our module to listen to certain events, they can be added into an array property of the class:
Listen property
The listen property is an array of events for each given module. The syntax for adding a listener to an event looks like so:
protected $listen = [
\Aero\Cart\Events\OrderSuccessful::class => [
\Aerocargo\Testing\Listeners\ExportOrder::class,
],
];
Custom directories
It is possible to create custom directories for different types of classes, for example, Traits, Jobs, or Helpers.
How do I create a custom module?
When interacting with the admin, especially when making a custom module, it is important to require aerocommerce/admin in the modules’ composer.json file.
Scaffolding modules
Modules can be created using artisan, a command line interface supplied with Laravel. The easiest way to create a new module is to open the terminal, navigate to the root folder of the project and paste in:
php artisan make:module vendor/module-name
It is important to stick to module naming conventions and include the vendor (such as aerocommerce or aerocargo) and the module name, with dashes replacing any space in the name.
Running the above command in the terminal results in a couple of brief messages, reporting on the status of the creation and installation process of our module. After all is complete, the module can be accessed in the root directory of Aero under “modules”.
Configuring modules
As mentioned above, each module that interacts with the admin needs to require aerocommerce/admin in the modules’ composer.json file.
Composer.json
{
"name": "aerocargo/new-module",
"description": "",
"require": {
"php": "^7.2|^8.0",
"aerocommerce/core": "^0",
"aerocommerce/admin": "^0"
},
"autoload": {
"psr-4": {
"Aerocargo\\NewModule\\": "src/"
}
},
"extra": {
"laravel": {
"providers": [
"Aerocargo\\NewModule\\ServiceProvider"
]
}
}
}
Service Provider
The module Service Provider ensures that our modules are connected to the rest of the platform through the setup() method that is automatically scaffolded into the class.
Adding modules to a visible list in the admin
Some modules might not require any interaction with the admin in terms of UI, so they don’t need to necessarily be listed in the modules section of the admin. If we wish to give our module an interface and allow users to access the module through the admin interface, we have to add the following code to our Service Provider’s setup() function:
AdminModule::create(‘new-module’)
->title('New Aero Module')
->summary('A brand new Aero module.')
->routes(__DIR__.'/../routes/admin.php')
->route('admin.new-module.index');
This module should now be listed in the admin.
Loading migrations
In order for the module to be able to detect all the migrations that a module has, it has to have a specified path in the modules’ Service Provider setup() function.
If the migration folder follows the original anatomy of module structure, all we have to do is paste the following code into the Service Provider setup() function:
$this->loadMigrationsFrom(__DIR__.'/../database/migrations');
You should now be able to call php artisan migrate in the root Aero directory to migrate data from the module.
Loading views
Loading views works the same way as loading migrations, all we have to do it to specify the path to our views and also a namespace.
$this->loadViewsFrom(__DIR__.'/../resources/views', 'new-module');
The second parameter in the above function is the namespace assigned to all of the module views. This is extra useful as we can then use that namespace to render views in the controller:
return view(‘new-module::index’);
Loading routes
There are two different types of routes that the module has access to. One of them is the admin routes which only give access to routes provided the user is an administrator. The other is the store routes which affect the store/frontend side of the system.
Admin routes
There are two ways of setting up admin routes, depending on whether we choose to use the AdminModule facade and load the module into the admin interface.
If we do choose to add our module to a list in the admin, apply the following chained function to the AdminModule facade in the setup() function in the Service Provider:
AdminModule::create('new-module')
->title('New Aero Module')
->summary('A brand new Aero module.')
->routes(__DIR__.'/../routes/admin.php');
This allows us to create the necessary admin routes, for navigating the module in the admin.
If we don’t choose to add our module to a list in the admin, we simply add the following piece of code to the setup() function in the module Service Provider:
Router::addAdminRoutes(__DIR__.'/../routes/admin.php');
Store routes
In order to add store (frontend) routes to the module, we simply add a piece of code to the setup() function of the module Service Provider:
Router::addStoreRoutes(__DIR__.’/../routes/store.php');
Asset Linking
In order to publish any resources from our module to the rest of Aero, we have to specify the path for those resources in a special function. This function is added to the module Service Provider:
public function assetLinks()
{
return [
'aerocargo/new-module' => __DIR__.'/../public',
];
}
After defining any asset paths to link, you'll need to run the command:
php artisan aero:link
How do I setup routes for my custom module?
There are a few necessary steps to create and properly configure module routing.
Creating routes
The first step to creating module routes is to create a directory named routes on the same level as the src directory – for more information refer to "Anatomy of a custom module". Depending on the type of the route, we need to create a .php file in the routes directory. If my module only requires admin routes, I’ll create a file called admin.php in the routes directory:
└─ module
└─ database
└─ migrations
└─ public
└─ resources
└─ css
└─ js
└─ views
└─ admin
└─ store
└─ routes
└─ admin.php
└─ src
└─ Http
└─ Controllers
└─ Responses
└─ Steps
└─ Requests
└─ Models
ServiceProvider.php
.gitignore
composer.json
README.md
The contents of the file become:
<?php
use Illuminate\Support\Facades\Route;
The file can then be populated with the necessary routes the module needs. For more information on routes see the laravel documentaiton on routing.
Loading routes in the Service Providers
The module needs to be made aware of all the routes that are available for it, and this is how it can be done:
Single routes file
$this->loadRoutesFrom(__DIR__.'/../routes/routes.php');
Store routes
Router::addStoreRoutes(__DIR__ . '/../routes/web.php');
Admin routes
Router::addAdminRoutes(__DIR__ . '/../routes/admin.php');
Using AdminModule facade
Routes can also be added through the AdminModule facade which allows the chaining for both route() and routes(). This should only be used with modules that have an interface/access point in the admin modules section. It can be done like so:
AdminModule::create('csv')
->title('CSV Import & Export')
->summary('Used to import and export a variety of platform data.')
->routes(__DIR__.'/../routes/admin.php')
->route('admin.csv.index');
Where the route() accesses a declared route that returns a view or routes(), with the same principle as the above examples.
How do I register a custom Vue component for my module
To register a Vue component for our module, we will need to create a JavaScript file for our components and in order to initialize Vue.
Link assets
In order to link all the assets, which is a necessary step of registering a Vue component, we need to add a function to the module Service Provider. This is the code we need in the service provider:
public function assetLinks()
{
return [
'vendor/module-name' => __DIR__ . '/../public',
];
}
Bear in mind this is the exact path we have to supply when loading components into a view.
Initialize components
import NewComponent from './components/NewComponent
window.newModule = {
install(Vue) {
Vue.component(‘new-component', NewComponent)
},
}
The above code gives us access to the <new-component></new-component> tag, provided we’ve loaded the components into a view.
Loading components into a view
@push('scripts')
<script src="{{ asset(mix(new-module.js', 'modules/aerocargo/new-module')) }}"></script>
<script>
window.AeroAdmin.vue.use(window.newModule);
</script>
@endpush
Provided all of the namespacing in the two files matches correctly, the Vue components that we have declared will now be accessible in our view.
If there is a problem with the mix of our assets, remember to run php artisan aero:link in the root directory of your Aero store.
Enabling Vue devtools
In order to enable Vue devtools for our module, we simply add the following line of code into the file where we initialize our components:
import NewComponent from './components/NewComponent
window.new-module = {
install(Vue) {
Vue.config.devtools = true;
Vue.component(‘new-component', NewComponent)
},
}
How do I add a custom field to the new and edit product page?
In this mini tutorial we will add a notes field to the product new and edit page for the product and each variant.
This mini tutorial assumes that you have a module setup or you’re happy working from the app service provider (using the boot method). You can see how to set up a module here (link).
Adding the Database Migration
To get started we will add a migration file that will update the products and variants tables to have a notes column. To do this we’ll need to create the migration file and load them from within the modules service provider.
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class AddNotesFieldsToProductsAndVariants extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('products', function (Blueprint $table) {
$table->string('notes')->nullable()->after('description');
});
Schema::table('variants', function (Blueprint $table) {
$table->string('notes')->nullable()->after('name');
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::table('products', function (Blueprint $table) {
$table->dropColumn('notes');
});
Schema::table('variants', function (Blueprint $table) {
$table->dropColumn('notes');
});
}
}
Adding the Slot View
Now we’ll create two views, one that will be injected for the product and one that will be injected for the variants. These views will have a text area in them for the notes input.
product-notes-field.blade.php
<div class="card mt-4">
<label for="notes" class="block mb-2">Notes</label>
<textarea name="notes" id="notes" v-model="product.notes" class="w-full"></textarea>
</div>
variant-notes-field.blade.php
<div class="p-4" v-if="!isSimpleProduct">
<label :for="'variant-notes-' + key" class="block mb-2">Notes</label>
<textarea :name="'variants[' + key + '][notes]'" :id="'variant-notes-' + key" v-model="variants[key].notes" class="w-full"></textarea>
</div>
After creating the views we will load them in the module service provider using the $this->loadViewsFrom method and inject them into the relevant slots using the Aero\Admin\AdminSlot::inject method.
<?php
namespace Acme\MyModule;
use Aero\Admin\AdminSlot;
use Aero\Common\Providers\ModuleServiceProvider;
class ServiceProvider extends ModuleServiceProvider
{
public function setup()
{
if ($this->app->runningInConsole()) {
$this->loadMigrationsFrom(__DIR__.'/../database/migrations');
}
$this->loadViewsFrom(__DIR__.'/../resources/views', 'my-module');
AdminSlot::inject('catalog.product.new.cards', 'my-module::product-notes-field');
AdminSlot::inject('catalog.product.edit.cards', 'my-module::product-notes-field');
AdminSlot::inject('catalog.product.new.variant', 'my-module::variant-notes-field');
AdminSlot::inject('catalog.product.edit.variant', 'my-module::variant-notes-field');
}
}
Making the Notes Save
Adding Notes as a Fillable
We need to make the notes fillable as Laravel will only mass assign fillable attributes. To make notes fillable we need to call the makeFillable method on the relevant models (Aero\Catalog\Models\Product and Aero\Catalog\Models\Variant).
<?php
namespace Acme\MyModule;
use Aero\Admin\AdminSlot;
use Aero\Catalog\Models\Product;
use Aero\Catalog\Models\Variant;
use Aero\Common\Providers\ModuleServiceProvider;
class ServiceProvider extends ModuleServiceProvider
{
public function setup()
{
if ($this->app->runningInConsole()) {
$this->loadMigrationsFrom(__DIR__.'/../database/migrations');
}
$this->loadViewsFrom(__DIR__.'/../resources/views', 'my-module');
AdminSlot::inject('catalog.product.new.cards', 'my-module::product-notes-field');
AdminSlot::inject('catalog.product.edit.cards', 'my-module::product-notes-field');
AdminSlot::inject('catalog.product.new.variant', 'my-module::variant-notes-field');
AdminSlot::inject('catalog.product.edit.variant', 'my-module::variant-notes-field');
Product::makeFillable('notes');
Variant::makeFillable('notes');
}
}
Adding Notes to the Validators
To let the notes input data get through validation it needs to be added to the Aero\Admin\Http\Requests\Catalog\CreateProductRequest and Aero\Admin\Http\Requests\Catalog\UpdateProductRequest validators. To do this we need to use the expects method on the two validators and pass in the notes field and its rules.
<?php
namespace Acme\MyModule;
use Aero\Admin\AdminSlot;
use Aero\Admin\Http\Requests\Catalog\CreateProductRequest;
use Aero\Admin\Http\Requests\Catalog\UpdateProductRequest;
use Aero\Catalog\Models\Product;
use Aero\Catalog\Models\Variant;
use Aero\Common\Providers\ModuleServiceProvider;
class ServiceProvider extends ModuleServiceProvider
{
public function setup()
{
if ($this->app->runningInConsole()) {
$this->loadMigrationsFrom(__DIR__.'/../database/migrations');
}
$this->loadViewsFrom(__DIR__.'/../resources/views', 'my-module');
AdminSlot::inject('catalog.product.new.cards', 'my-module::product-notes-field');
AdminSlot::inject('catalog.product.edit.cards', 'my-module::product-notes-field');
AdminSlot::inject('catalog.product.new.variant', 'my-module::variant-notes-field');
AdminSlot::inject('catalog.product.edit.variant', 'my-module::variant-notes-field');
Product::makeFillable('notes');
Variant::makeFillable('notes');
CreateProductRequest::expects('notes', 'nullable|string');
UpdateProductRequest::expects('notes', 'nullable|string');
}
}
Adding Notes to the Transformers
Adding notes to the transformers will ensure that it’s available in our views through Vue. We need to use the add method on the Aero\Admin\Transformers\BaseVariantTransformer, Aero\Admin\Transformers\ProductTransformer, and Aero\Admin\Transformers\VariantTransformer transformers.
<?php
namespace Acme\MyModule;
use Aero\Admin\AdminSlot;
use Aero\Admin\Http\Requests\Catalog\CreateProductRequest;
use Aero\Admin\Http\Requests\Catalog\UpdateProductRequest;
use Aero\Admin\Transformers\BaseVariantTransformer;
use Aero\Admin\Transformers\ProductTransformer;
use Aero\Admin\Transformers\VariantTransformer;
use Aero\Catalog\Models\Product;
use Aero\Catalog\Models\Variant;
use Aero\Common\Providers\ModuleServiceProvider;
class ServiceProvider extends ModuleServiceProvider
{
public function setup()
{
if ($this->app->runningInConsole()) {
$this->loadMigrationsFrom(__DIR__.'/../database/migrations');
}
$this->loadViewsFrom(__DIR__.'/../resources/views', 'my-module');
AdminSlot::inject('catalog.product.new.cards', 'my-module::product-notes-field');
AdminSlot::inject('catalog.product.edit.cards', 'my-module::product-notes-field');
AdminSlot::inject('catalog.product.new.variant', 'my-module::variant-notes-field');
AdminSlot::inject('catalog.product.edit.variant', 'my-module::variant-notes-field');
Product::makeFillable('notes');
Variant::makeFillable('notes');
CreateProductRequest::expects('notes', 'nullable|string');
UpdateProductRequest::expects('notes', 'nullable|string');
ProductTransformer::add(function ($data) {
return [
'notes' => $data['product']->notes ?? '',
];
});
VariantTransformer::add(function ($data) {
return [
'notes' => $data['variant']->notes ?? '',
];
});
BaseVariantTransformer::add(function ($data) {
return [
'notes' => '',
];
});
}
}
How do I add a custom price to a product with a module?
In this mini tutorial we will add some checkboxes to the product page that when checked add an additional charge to the product. The additional charge will be shown dynamically on the product page as it changes or the product's price changes (due to different variants being selected).
This mini tutorial assumes that you have a module setup or you’re happy working from the app service provider (using the boot method). You can see how to set up a module here (link).
Adding the Prices Data
To get started we will add a $prices array to our module service provider that will be the source of truth for our extra prices. We are using a simple array instead of database data due to this being a mini tutorial. The $prices array will have id, name, and price (this will also have inc and ex values and be in pence) keys.
<?php
namespace Acme\MyModule;
use Aero\Common\Providers\ModuleServiceProvider;
class ServiceProvider extends ModuleServiceProvider
{
protected $prices = [
[
'id' => 1,
'name' => 'Pay £1 more',
'price' => [
'inc' => 100,
'ex' => 83.33,
],
],
[
'id' => 2,
'name' => 'Pay £5 more',
'price' => [
'inc' => 500,
'ex' => 416.6,
],
],
[
'id' => 3,
'name' => 'Pay £10 more',
'price' => [
'inc' => 1000,
'ex' => 833.33,
],
],
];
public function setup()
{
//
}
}
Creating a Javascript View and Adding the Prices Data and Javascript to the Product Page
Now we’re going to create a Twig file called javascript where we will put the frontend javascript that will be used on the product page. This view is registered using the $this->loadViewsFrom method in the module service providers setup method.
Next we’re going to use the Aero\Store\Http\Responses\ProductPage response builder to extend the product page. We will inject the $prices data using the setData method and then we’ll use the Aero\Store\Pipelines\ContentForBody pipeline to add our javascript view to the product page.
<?php
namespace Acme\MyModule;
use Aero\Common\Providers\ModuleServiceProvider;
use Aero\Store\Http\Responses\ProductPage;
use Aero\Store\Pipelines\ContentForBody;
class ServiceProvider extends ModuleServiceProvider
{
protected $prices = [
[
'id' => 1,
'name' => 'Pay £1 more',
'price' => [
'inc' => 100,
'ex' => 83.33,
],
],
[
'id' => 2,
'name' => 'Pay £5 more',
'price' => [
'inc' => 500,
'ex' => 416.6,
],
],
[
'id' => 3,
'name' => 'Pay £10 more',
'price' => [
'inc' => 1000,
'ex' => 833.33,
],
],
];
public function setup()
{
$this->loadViewsFrom(__DIR__.'/../resources/views', 'my-module');
ProductPage::extend(function (ProductPage $page) {
$page->setData('extra_prices', $this->prices);
ContentForBody::extend(function (&$content) {
$content .= view('my-module::javascript');
});
});
}
}
Adding the Extra Prices to the Product Page
In the product.twig file we’re going to add some Vue code to show the total price and the total extra price. Then we’ll add some Twig code to loop over the extra_prices variable that we injected into the product page earlier. This loop will display the name as a label and then add a checkbox with a value of the extra price id and some data attributes for the price inc and ex values.
Total Price
<p v-text="total_price.inc"></p>
Total Extra Price
<p v-text="additional_prices['extra-prices'].inc" v-if="additional_prices['extra-prices']"></p>
{% for extra in extra_prices %}
<div>
<input type="checkbox" id="extraPrice{{ extra.id }}" value="{{ extra.id }}" data-extra-price-inc="{{ extra.price.inc }}" data-extra-price-ex="{{ extra.price.ex }}">
<label for="extraPrice{{ extra.id }}"> {{ extra.name }}</label>
</div>
{% endfor %}
Coding the Javascript for the Product Page
Now we’re going to add javascript code to the javascript twig view we previously created. This javascript code will be responsible for setting the additional price when the checkboxes are checked and sending any checked extra prices to the server when the product is added to the cart.
To update the additional price when the checkboxes are checked we’ll set some code to be executed on the product.loaded Aero Event. This code will loop over all input checkbox elements that have the data-extra-price-inc attribute and add an input event listener. We’ll add a updateAndSetAdditionalPrices function that will be executed when the input changes and also initially once the product has loaded (in case you have some extra prices that are checked by default). This function loops over all input checked checkboxes that have the data-extra-price-inc attribute and adds up their inc and ex prices. Then it runs the product.set-additional-price Aero Event to tell Aero the new total additional price.
To send the checked extra prices to the server we’ll set some code to be executed on the product.add-to-cart Aero Event. This code loops over all input checked checkboxes that have the data-extra-price-inc attribute and adds their values (the extra price id) to an array. This array is then added to the payload that will be sent to the server.
<script>
window.AeroEvents.on('product.loaded', function () {
var extraPriceElements = document.querySelectorAll('input[type="checkbox"][data-extra-price-inc]');
for (var i = 0; i < extraPriceElements.length; i++) {
extraPriceElements[i].addEventListener('input', updateAndSetAdditionalPrices);
}
function updateAndSetAdditionalPrices() {
var price = { key: 'extra-prices', inc: 0, ex: 0 };
var extraPriceElements = document.querySelectorAll('input[type=checkbox]:checked[data-extra-price-inc]');
for (var i = 0; i < extraPriceElements.length; i++) {
price.inc += parseInt(extraPriceElements[i].dataset.extraPriceInc);
price.ex += parseInt(extraPriceElements[i].dataset.extraPriceEx);
}
window.Aero.runEvent('product.set-additional-price', price);
}
updateAndSetAdditionalPrices();
});
window.AeroEvents.on('product.add-to-cart', function (data) {
var extras = [];
var extraPriceElements = document.querySelectorAll('input[type=checkbox]:checked[data-extra-price-inc]');
for (var i = 0; i < extraPriceElements.length; i++) {
extras.push(extraPriceElements[i].value);
}
data.extras = extras;
return data;
});
</script>
Adding any Selected Extra Prices to the Cart Item
We’ll add some code in the module service provider below the current code that extends the Aero\Store\Pipelines\CartItemBuilder and gets the extra price ids from the request, maps them to the relevant extra price, filters out any that are null (because they were not valid extra price ids), and then adds a Aero\Cart\CartItemOption to the Aero\Cart\CartItem with the extra price details.
<?php
namespace Acme\MyModule;
use Aero\Cart\CartItem;
use Aero\Cart\CartItemOption;
use Aero\Common\Providers\ModuleServiceProvider;
use Aero\Store\Http\Responses\ProductPage;
use Aero\Store\Pipelines\CartItemBuilder;
use Aero\Store\Pipelines\ContentForBody;
class ServiceProvider extends ModuleServiceProvider
{
protected $prices = [
[
'id' => 1,
'name' => 'Pay £1 more',
'price' => [
'inc' => 100,
'ex' => 83.33,
],
],
[
'id' => 2,
'name' => 'Pay £5 more',
'price' => [
'inc' => 500,
'ex' => 416.6,
],
],
[
'id' => 3,
'name' => 'Pay £10 more',
'price' => [
'inc' => 1000,
'ex' => 833.33,
],
],
];
public function setup()
{
$this->loadViewsFrom(__DIR__.'/../resources/views', 'my-module');
ProductPage::extend(function (ProductPage $page) {
$page->setData('extra_prices', $this->prices);
ContentForBody::extend(function (&$content) {
$content .= view('my-module::javascript');
});
});
CartItemBuilder::extend(function (CartItem $item) {
collect(request()->input('extras', []))->map(function ($extra) {
return collect($this->prices)->firstWhere('id', $extra);
})->filter()->each(function ($extra) use ($item) {
$option = CartItemOption::create($extra['name']);
$option->setPriceInc($extra['price']['inc']);
$item->addOption($option);
});
});
}
}
How do I add settings to my model?
This tutorial will explain the steps required to add settings to your model and assumes you have your model setup with create and edit pages.
Adding a Trait to your Model
The first step is to add the Aero\Common\Traits\CanHaveSettings trait to your model.
<?php
namespace Acme\MyModule\Models;
use Aero\Common\Models\Model;
use Aero\Common\Traits\CanHaveSettings;
class MyModel extends Model
{
use CanHaveSettings;
}
Defining the Settings for your Model
Now that your model has the Aero\Common\Traits\CanHaveSettings trait you can use the static settings() function on your models class to define your settings.
The settings are defined in the same way as when you create a normal setting group, the only difference being that you use your models class instead of the normal settings facade. You can read more about settings here.
<?php
namespace Acme\MyModule;
use Acme\MyModule\Models\MyModel;
use Aero\Common\Providers\ModuleServiceProvider;
use Aero\Common\Settings\SettingGroup;
class ServiceProvider extends ModuleServiceProvider
{
public function setup()
{
MyModel::settings(function (SettingGroup $group) {
$group->encrypted('password');
$group->boolean('require_password_to_view')->default(false);
});
}
}
Adding the Settings Fields to your Create/Edit Pages
Now that you have defined the settings for your model you can update your models create/edit pages to let users update the settings.
To add a card that lets user edit the models settings to your create/edit pages you need to include the admin::settings.model-settings view and pass in your model, like this:
@include('admin::settings.model-settings', ['model' => $myModel])
If you don’t have a model to pass in (because you’re on a create page so the model hasn’t been created yet), pass in a fresh instance of your model, like this:
@include('admin::settings.model-settings', ['model' => new \Acme\MyModule\Models\MyModel()])
It’s important to ensure that the include is inside of your form. A more “complete” example may look something like this:
@extends('admin::layouts.main')
@section('content')
<div class="max-w-2xl mx-auto">
<div class="flex w-full justify-between">
<h2><a href="{{ route('admin.modules', request()->all()) }}" class="btn mr-4">@include('admin::icons.back') Back</a> Managing My Model</h2>
</div>
@include('admin::partials.alerts')
<form action="#" method="post" class="flex flex-wrap">
@csrf
@method('put')
<fieldset class="w-full">
{{-- Your other fields etc--}}
@include('admin::settings.model-settings', ['model' => $myModel])
<div class="form-buttons fieldset-disabled-hide">
<div class="card w-full">
<button class="btn btn-secondary" type="submit">Save</button>
</div>
</div>
</fieldset>
</form>
</div>
@endsection
Adding the Settings Validation to your Create/Edit Requests
You need to update your request validators so that the settings data will be correctly validated and formatted.
Adding the Rules
You need to update your validators rules method to merge in the settings rules with your current rules. To do this you need to array_merge your rules with the array of rules returned by the Aero\Admin\Utils\SettingHelpers::getRulesForModel method. The Aero\Admin\Utils\SettingHelpers::getRulesForModel method expects you to pass the class string of your model.
<?php
namespace Acme\MyModule\Requests;
use Acme\MyModule\Models\MyModel;
use Aero\Admin\Utils\SettingHelpers;
use Aero\Common\Requests\AeroRequest;
class StoreMyModelRequest extends AeroRequest
{
public function rules(): array
{
return array_merge([
'name' => 'required|max:255', // You can add your models settings here
], SettingHelpers::getRulesForModel(MyModel::class));
}
}
Adding the Attributes
You need to do the same thing for the attributes.
<?php
namespace Acme\MyModule\Requests;
use Acme\MyModule\Models\MyModel;
use Aero\Admin\Utils\SettingHelpers;
use Aero\Common\Requests\AeroRequest;
class StoreMyModelRequest extends AeroRequest
{
public function attributes(): array
{
return array_merge([], SettingHelpers::getRuleAttributesForModel(MyModel::class));
}
public function rules(): array
{
return array_merge([
'name' => 'required|max:255', // You can add your models settings here
], SettingHelpers::getRulesForModel(MyModel::class));
}
}
Formatting the Data for Validation
You need to add a prepareForValidation method to your validator and ensure it calls the SettingHelpers::formatRequestDataForModel method like shown below. This method will format the incoming settings request data so that it is ready to be validated.
<?php
namespace Acme\MyModule\Requests;
use Acme\MyModule\Models\MyModel;
use Aero\Admin\Utils\SettingHelpers;
use Aero\Common\Requests\AeroRequest;
class StoreMyModelRequest extends AeroRequest
{
public function prepareForValidation()
{
$this->replace(
SettingHelpers::formatRequestDataForModel(MyModel::class, $this->all())
);
}
public function attributes(): array
{
return array_merge([], SettingHelpers::getRuleAttributesForModel(MyModel::class));
}
public function rules(): array
{
return array_merge([
'name' => 'required|max:255', // You can add your models settings here
], SettingHelpers::getRulesForModel(MyModel::class));
}
}
Saving the Validated Settings for your Model
To save the settings you need to pass your model and data into the Aero\Admin\Utils\SettingHelpers::saveForModel method, like this:
<?php
namespace Acme\MyModule\Http\Controllers;
use Acme\MyModule\Models\MyModel;
use Acme\MyModule\Requests\UpdateMyModelRequest;
use Aero\Admin\Http\Controllers\Controller;
use Aero\Admin\Utils\SettingHelpers;
class MyModelController extends Controller
{
public function update(UpdateMyModelRequest $request, MyModel $model)
{
$model->update($data = $request->validated());
SettingHelpers::saveForModel($model, $request->validated()['settings'] ?? []);
return redirect(route('my-model.index'))->with([
'message' => __('Your changes have been saved'),
]);
}
}
How do I extend the address forms?
Aero has address forms in the checkout, account-area, and admin that can be extended.
Storefront Address Forms
The storefront address forms are address forms found on your storefront (the checkout and the account-area). These forms are customer facing and all extend Aero\Forms\AddressForm.
Structure
In all of the address forms there’s a top_sections, sections, and bottom_sections section (some forms have an additional section). These sections build up the form. The reason that there are sections is so when the form is in lookup mode, the address fields can be hidden as they’re all in the sections section.
Top Sections
Key | View |
first_name | forms::address.fields.first-name |
last_name | forms::address.fields.last-name |
lookup | forms::address.lookup |
Sections
Key | View |
company | forms::address.fields.company |
line1 | forms::address.fields.line1 |
line2 | forms::address.fields.line2 |
city | forms::address.fields.city |
zone | forms::address.fields.zone |
postcode | forms::address.fields.postcode |
country | forms::address.fields.country |
Bottom Sections
Key | View |
phone | forms::address.fields.phone |
Additional Sections
Some storefront address forms have some additional sections on top of the ones above. To adjust these sections you’d have to specifically extend the specific form class instead of the generic Aero\Forms\AddressForm class.
Aero\Checkout\Http\Forms\CustomerAddressForm
This address form has additional customer sections.
Key | View |
customer | account-area::forms.fields.address.name |
Aero\AccountArea\Http\Forms\AccountAddressForm
This address form is extended by all of the other account area address forms. This form injects 2 additional things to the top and bottom sections.
The top sections has this injected right at the top of all of the top sections:
Key | View |
address_name | checkout::sections.customer |
The bottom sections has this injected right at the bottom of all of the bottom sections:
Key | View |
is_default | account-area::forms.fields.address.is_default |
Admin Address Forms
The admin address forms are address forms found on your admin. These forms all extend Aero\Admin\Http\Forms\AdminAddressForm.
Structure
The structure for the admin forms is the same as the structure for the storefront forms but the views are different.
Top Sections
Key | View |
first_name | admin::forms.fields.address.first-name |
last_name | admin::forms.fields.address.last-name |
lookup | admin::forms.partials.address-lookup |
Sections
Key | View |
company | admin::forms.fields.address.company |
line1 | admin::forms.fields.address.line1 |
line2 | admin::forms.fields.address.line2 |
city | admin::forms.fields.address.city |
zone | admin::forms.fields.address.zone |
postcode | admin::forms.fields.address.postcode |
country | admin::forms.fields.address.country |
Bottom Sections
Key | View |
phone | admin::forms.fields.address.phone |
Address Form Validation
All address forms take validation from Aero\Forms\Validation\AddressRules. Instead of adding rules to this class through the extends static method you should make use of the Aero\Common\Helpers\Address::addField() method. This method accepts the same parameters as if you were extending a validator (you can learn more about that here, link). The difference is that it adds the rules but also makes your field fillable on all of the address models. These models are Aero\Account\Models\Address, Aero\Cart\Models\OrderAddress, and Aero\Fulfillment\Models\FulfillmentAddress.
How to add a custom field to the address forms?
This short tutorial will walk you through adding an address line 3 field to all of the address forms (found in the admin, account-area, and checkout).
Migrations
The first step is to add your field to all of the address tables so that it can be stored. To do this we’ll create and register a migration that adds a line_3 field to the addresses, order_addresses, and fulfillment_addresses tables.
Migration Code
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class AddLine3ToAddressTables extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('addresses', function (Blueprint $table) {
$table->string('line_3')->nullable()->after('line_2');
});
Schema::table('order_addresses', function (Blueprint $table) {
$table->string('line_3')->nullable()->after('line_2');
});
Schema::table('fulfillment_addresses', function (Blueprint $table) {
$table->string('line_3')->nullable()->after('line_2');
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::table('fulfillment_addresses', function (Blueprint $table) {
$table->dropColumn('line_3');
});
Schema::table('order_addresses', function (Blueprint $table) {
$table->dropColumn('line_3');
});
Schema::table('addresses', function (Blueprint $table) {
$table->dropColumn('line_3');
});
}
}
Service Provider Code
<?php
namespace Acme\MyModule;
use Aero\Common\Providers\ModuleServiceProvider;
class ServiceProvider extends ModuleServiceProvider
{
public function setup()
{
if ($this->app->runningInConsole()) {
$this->loadMigrationsFrom(__DIR__.'/../database/migrations');
}
}
}
Views
The next step is to create a view for the line 3 field. To achieve this we’ll create and register a Twig view for the field. The view will include the forms::components.input view to get a simple input field.
View Code
{% include "forms::components.input" with {
type: 'text',
error: errors.first((inputName ?? group) ~ '.line_3'),
half: false,
label: 'Address Line 3 (Optional)',
class: (inputName ?? group) ~ '-' ~ 'line-3',
name: (inputName ?? group) ~ '[line_3]',
value: old((inputName ?? group) ~ '.line_3', address.line_3),
autocomplete: type ? type ~ ' line_3' : 'line_3',
required: true
} only %}
Service Provider Code
<?php
namespace Acme\MyModule;
use Aero\Common\Providers\ModuleServiceProvider;
class ServiceProvider extends ModuleServiceProvider
{
public function setup()
{
if ($this->app->runningInConsole()) {
$this->loadMigrationsFrom(__DIR__.'/../database/migrations');
}
$this->loadViewsFrom(__DIR__.'/../resources/views', 'my-module');
}
}
Adding the Field to the Forms
Now we need to add the field to the address forms. To do this we’ll make the field fillable and add it to the validation requests using the Aero\Common\Helpers\Address::addField() helper method. After that we’ll extend Aero\Forms\AddressForm to add the field to the frontend address forms (checkout and account-area) and Aero\Admin\Http\Forms\AdminAddressForm to add the field to the admin address forms.
<?php
namespace Acme\MyModule;
use Aero\Admin\Http\Forms\AdminAddressForm;
use Aero\Common\Helpers\Address;
use Aero\Common\Providers\ModuleServiceProvider;
use Aero\Forms\AddressForm;
class ServiceProvider extends ModuleServiceProvider
{
public function setup()
{
if ($this->app->runningInConsole()) {
$this->loadMigrationsFrom(__DIR__.'/../database/migrations');
}
$this->loadViewsFrom(__DIR__.'/../resources/views', 'my-module');
Address::addField('line_3');
AddressForm::extend(function ($form) {
$form->addSectionAfter('line3', 'my-module::line-3-field', 'line2');
});
AdminAddressForm::extend(function ($form) {
$form->addSectionAfter('line3', 'my-module::line-3-field', 'line2');
});
}
}
Adding the Field to the Rendered Addresses
This step is optional and will add the line 3 field to the address when it’s rendered (such as when viewing an order or viewing your addresses in the account area). To do this we’ll extend Aero\Address\Pipelines\AddressFormatter and Aero\Address\Pipelines\AddressStringFormatter.
<?php
namespace Acme\MyModule;
use Aero\Address\Pipelines\AddressFormatter;
use Aero\Address\Pipelines\AddressStringFormatter;
use Aero\Admin\Http\Forms\AdminAddressForm;
use Aero\Common\Helpers\Address;
use Aero\Common\Providers\ModuleServiceProvider;
use Aero\Forms\AddressForm;
class ServiceProvider extends ModuleServiceProvider
{
public function setup()
{
if ($this->app->runningInConsole()) {
$this->loadMigrationsFrom(__DIR__.'/../database/migrations');
}
$this->loadViewsFrom(__DIR__.'/../resources/views', 'my-module');
Address::addField('line_3');
AddressForm::extend(function ($form) {
$form->addSectionAfter('line3', 'my-module::line-3-field', 'line2');
});
AdminAddressForm::extend(function ($form) {
$form->addSectionAfter('line3', 'my-module::line-3-field', 'line2');
});
AddressFormatter::extend(function ($formatter) {
$formatter->putAfter('line3', $formatter->fields['line_3'], 'line2');
});
AddressStringFormatter::extend(function ($formatter) {
$formatter->putAfter('line3', $formatter->fields['line_3'], 'line2');
});
}
}
What are models and how do I use them?
All models provided as part of the Aero core platform are Eloquent models. These can be extended with custom methods, which will typically interact with the model they are attached to in some way. This functionality is especially useful when creating custom Eloquent relationships.
For example, if a module provided reviews for products, the reviews method can be added to the Product model using a macro:
\Aero\Catalog\Models\Product::macro('reviews', function () {
return $this->hasMany(\Acme\MyModule\Models\Review::class);
});
The relationship query builder can be accessed by referencing the method:
$approvedReviewCount = $product->reviews()->where('approved', true)->count();
Just like a typical Eloquent relationship on a model, the resulting Collection of reviews can be accessed through the magic property:
$reviews = $product->reviews;
{% for review in product.reviews %}
...
{% endfor %}
How do I remove the manufacturer from the product slug/url?
The manufacturer name is automatically prepended to the product slug (its URL). If you wish for this to not happen you can set the static Aero\Catalog\Models\Product::$slugContainsManufacturer boolean to false.
<?php
namespace Acme\MyModule;
use Aero\Catalog\Models\Product;
use Aero\Common\Providers\ModuleServiceProvider;
class ServiceProvider extends ModuleServiceProvider
{
public function setup()
{
Product::$slugContainsManufacturer = false;
}
}
How do I remove the manufacturers name from the order items name?
By default if a product name doesn’t contain the manufacturer name already, the manufacturer name is appended to the front of the product name when converting a cart item to an order item.
If you don’t want the manufacturer name appended to the product name you can set the static Aero\Cart\Models\OrderItem::$nameContainsManufacturer boolean to false.
<?php
namespace Acme\MyModule;
use Aero\Cart\Models\OrderItem;
use Aero\Common\Providers\ModuleServiceProvider;
class ServiceProvider extends ModuleServiceProvider
{
public function setup()
{
OrderItem::$nameContainsManufacturer = false;
}
}