Developer Documentation
Developer Documentation

Account Area
Account Area

What are the default pages in the account area?
The following classes are in the Aero\AccountArea\Http\Responsesnamespace.
Class
Route
Description
AccountLoginPage
login
A page where users can log in.
AccountRegisterPage
register
A page where guests can register.
AccountPasswordForgotPage
password.forgot
A page where users can send a password reset email to their email.
AccountPasswordResetPage
password.reset
A page where users can reset their password.
AccountOrdersPage
account.orders
A page that lists all of a user's orders and lets them filter/search.
AccountOrderViewPage
account.view-order
A page that shows more information about an order.
AccountAddressesPage
account.addresses
A page that lists all of the users addresses.
AccountAddressNewPage
account.new-address
A page where users can create a new address,
AccountAddressEditPage
account.edit-address
A page where users can edit one of their addresses.
AccountDetailsPage
account.details
A page where users can update their account details and change their password.
AccountInvoicePage
account.view-invoice
A simple invoice for an order - currently not linked to from anywhere.
AccountOverviewPage
account
The first page a user sees after they log in - currently redirects to the orders page.

What are the default sets in the account area?
The following classes are in the Aero\AccountArea\Http\Responsesnamespace.
Class
Method
Route
AccountLoginSet
Post
login
AccountLogoutSet
Post
logout
AccountRegisterSet
Post
register
AccountPasswordForgotSet
Post
password.forgot
AccountPasswordResetSet
Post
password.reset
AccountAddressNewSet
Post
account.new-address
AccountAddressEditSet
Put
account.edit-address
AccountAddressMakeDefaultSet
Put
account.make-default-address
AccountDetailsSet
Put
account.details
AccountAddressDelete
Delete
account.delete-address

What are the default forms in the account area?
The following classes are in the Aero\AccountArea\Http\Formsnamespace.
Class
Description
AccountAddressEditForm
The edit address form.
AccountAddressNewForm
The new address form.
AccountDetailsForm
The account details form.
AccountLoginForm
The login form.
AccountPasswordForgotForm
The forgotten password form.
AccountPasswordResetForm
The password reset form.
AccountRegisterForm
The register form.

How do I inject into a slot in the account area?
You can inject into account area slots using the Aero\AccountArea\AccountAreaSlot::injectmethod. This method accepts two parameters, the key of the slot you want to inject into and then the view or a closure that defines the view to be injected. When passing a closure it enables you to add additional view data to use inside of your injected view.
<?php
namespace Acme\MyModule;
use Aero\AccountArea\AccountAreaSlot;
use Aero\Common\Providers\ModuleServiceProvider;
class ServiceProvider extends ModuleServiceProvider
{
   public function setup()
   {
       $this->loadViewsFrom(__DIR__.'/../resources/views', 'my-module');
       AccountAreaSlot::inject('orders.order.card.header', 'my-module::account-area-slot');
       AccountAreaSlot::inject('orders.order.card.header', function ($data) {
           $data['custom_data'] = 'custom value';
           return view('my-module::account-area-slot', $data);
       });
   }
}

How do I configure the account area?
ENV Variables
Variable
Default
Description
AERO_ACCOUNT_AREA_THEME
aerocommerce/account-area
Defines the namespace that should be registered as having the views and resources for the account area. This should only be changed under unique circumstances.
AERO_ACCOUNT_AREA_LAYOUT
layouts.main
Defines the layout that the account area views use. If the layout provided doesn’t exist, account-area::layouts.blank will be used.
Config File
If you would like to change any of the config values you can publish the config file by running:
php artisan vendor:publish --provider="Aero\AccountArea\ServiceProvider"
Once the file is published you will be able to edit it through the config php file created at /config/aero/account-area.php.
Date Format
This defines the format used when displaying any dates in the account area.
Order Status State Classes
This defines the map used to find the colour for the order status state.
The colours are defined using CSS classes:
Class
Colour
success
Green
warning
Orange
error
Red
Order Status Radio Options
This defines the order statuses shown in the radio options on the orders page and what order status states they map to.
Order Years
This defines the number of previous years to display in the years dropdown on the orders page.
Page Sections
This defines the sections that make up each page by default. You can add/edit/remove sections through code, it is not necessary to adjust these values usually.
Slots
This defines the views that are injected into slots by default.
Links
This defines the links that should be in the navbar by default. You can add/edit/remove links through code, it is not necessary to adjust these values usually.

How do I create a custom page in the account area?
To create and register a custom account area page you need to use the **Aero\AccountArea\AccountArea::registerPage()**helper method. This method expects you to pass a class that extends Aero\AccountArea\AccountAreaPage.
<?php
namespace Acme\MyModule;
use Acme\MyModule\AccountArea\Pages\CustomPage;
use Aero\AccountArea\AccountArea;
use Aero\Common\Providers\ModuleServiceProvider;
class ServiceProvider extends ModuleServiceProvider
{
   public function setup()
   {
       AccountArea::registerPage(CustomPage::class);
   }
}
Your account area page class must implement the title, route, and routeNamemethods that all should return a string. It’s common practice to use Laravel Localization for the title and route so that they can change depending on the language used. You should not use this for the routeName as the routeName is used in code to refer to the route and therefore should never change.
You should also define a protected static steps and middleware array and a protected sections array. The static steps array can be used to define any extra response steps required for your page. The middleware array can be used to define any middleware required for your page (the most useful being ‘account’ which means users need to be logged in to view your page). The sections array defines the views that make up your page.
<?php
namespace Acme\MyModule\AccountArea\Pages;
use Aero\AccountArea\AccountAreaPage;
class CustomPage extends AccountAreaPage
{
   protected static $steps = [
       Steps\AttachWishlist::class,
   ];
   protected static $middleware = [
       'account',
   ];
   protected $sections = [
       'navbar' => 'account-area::sections.navbar',
       'page-header' => 'account-area::partials.page-header',
       'wishlist' => 'wishlist::account-area-wishlist',
   ];
   static function title(): string
   {
       return __('My Custom Page');
   }
   static function route(): string
   {
       return __('custom-page');
   }
   static function routeName(): string
   {
       return 'acme.my-module.custom-page';
   }
}
Adding your Page to the Navbar
To add your page to the navbar you need to make sure your page class uses the Aero\AccountArea\Traits\HasAccountAreaLinktrait and then add the link to the navbar using the addLink, addLinkBefore, or addLinkAftermethods from within the pipeline.
The Aero\AccountArea\Traits\HasAccountAreaLinktrait makes you implement a toLinkmethod that returns an array of data used to render your link. The array should have an icon and text key.
public static function toLink(): array
{
   return [
       'icon' => 'my-module::icon',
       'text' => 'My Page',
   ];
}
<?php
namespace Acme\MyModule;
use Acme\MyModule\AccountArea\Pages\CustomPage;
use Aero\AccountArea\AccountAreaPage;
use Aero\Common\Providers\ModuleServiceProvider;
class ServiceProvider extends ModuleServiceProvider
{
   public function setup()
   {
       AccountAreaPage::extend(function ($page) {
           $page->addLink(CustomPage::class);
       });
   }
}

How do I extend a form in the account area?
Forms can be extended using a pipeline from your modules service providers setup method or your projects app service providers boot method. You can read more about pipelines in the article "Introduction to pipelines
Adding a Field
To add fields to a form you can use the addSection, addSectionBefore, or addSectionAftermethods from within the pipeline.
addSection Method
This method accepts a key and a view to be injected. A field added with this method will be added as the first field.
addSectionBefore Method
This method accepts the same parameters as the addSectionmethod but additionally accepts a third parameter, the key of the field this new field should be added before.
addSectionAfter Method
This method accepts the same parameters as the addSectionmethod but additionally accepts a third parameter, the key of the field this new field should be added after.
Finding the Current Field Keys
If you do not know the keys of the current fields you can use this code snippet to dump the current keys when visiting the page with the form.
<?php
namespace Acme\MyModule;
use Aero\AccountArea\Http\Forms\AccountAddressForm;
use Aero\Common\Providers\ModuleServiceProvider;
class ServiceProvider extends ModuleServiceProvider
{
   public function setup()
   {
       AccountAddressForm::extend(function ($form) {
           dd($form->getSections()->keys()->toArray());
       });
   }
}
<?php
namespace Acme\MyModule;
use Aero\AccountArea\Http\Forms\AccountAddressForm;
use Aero\Common\Providers\ModuleServiceProvider;
class ServiceProvider extends ModuleServiceProvider
{
   public function setup()
   {
       AccountAddressForm::extend(function ($form) {
           $form->addSection('my-section', 'my-view');
       });
   }
}
Removing a Field
You can use the removeSectionmethod within the pipeline to remove a field from a form. The methods expect one parameter, the key of the field that you would like to remove.
<?php
namespace Acme\MyModule;
use Aero\AccountArea\Http\Forms\AccountAddressForm;
use Aero\Common\Providers\ModuleServiceProvider;
class ServiceProvider extends ModuleServiceProvider
{
   public function setup()
   {
       AccountAddressForm::extend(function ($form) {
           $form->removeSection('line2');
       });
   }
}

How do I create a custom set in the account area?
Sets are used for POST/PUT/DELETE requests. To create and register a custom account area set you need to use the Aero\AccountArea\AccountArea::registerPost(), Aero\AccountArea\AccountArea::registerPut(), or Aero\AccountArea\AccountArea::registerDelete(), helper method. This method expects you to pass a class that extends Aero\AccountArea\AccountAreaSet.
<?php
namespace Acme\MyModule;
use Acme\MyModule\AccountArea\Pages\CustomSet;
use Aero\AccountArea\AccountArea;
use Aero\Common\Providers\ModuleServiceProvider;
class ServiceProvider extends ModuleServiceProvider
{
   public function setup()
   {
       AccountArea::registerPost(CustomSet::class);
   }
}
Your account area set class must implement the routeand routeNamemethods that all should return a string. You should also define a protected static steps and middleware array. The steps array can be used to define any extra response steps required for your set. The middleware array can be used to define any middleware required for your set (the most useful being ‘account’ which means the user needs to be logged in).
<?php
namespace Acme\MyModule\AccountArea\Pages;
use Aero\AccountArea\AccountAreaSet;
class CustomSet extends AccountAreaSet
{
   protected static $steps = [
       Steps\DeleteWishlist::class,
   ];
   protected static $middleware = [
       'account',
   ];
   static function route(): string
   {
       return 'wishlist/delete';
   }
   static function routeName(): string
   {
       return 'acme.my-module.wishlist.delete';
   }
}

How do I extend a page or set in the account area?
Account Area Pages/Sets are Response Builders (link) which means that they can be extended using a pipeline. You can use all of the usual Response Builder stuff and also for pages you can add/remove/update sections using addSection, addSectionAfter, addSectionBefore, and removeSection.
addSection Method
This method accepts a key string and a view string. A section added with this method will be added as the first section.
addSectionBefore Method
This method accepts the same parameters as the addSectionmethod but additionally accepts a third parameter, the key of the section this new section should be added before.
addSectionAfter Method
This method accepts the same parameters as the addSectionmethod but additionally accepts a third parameter, the key of the section this new section should be added after.
removeSection Method
This method accepts one parameter, the key of the section to remove.
Finding the Current Section Keys
If you do not know the keys of the current sections you can use this code snippet to dump the current keys out when visiting the page.
<?php
namespace Acme\MyModule;
use Aero\AccountArea\Http\Responses\AccountOrdersPage;
use Aero\Common\Providers\ModuleServiceProvider;
class ServiceProvider extends ModuleServiceProvider
{
   public function setup()
   {
       AccountOrdersPage::extend(function (AccountOrdersPage $page) {
           dd($page->getSections()->keys());
       });
   }
}
<?php
namespace Acme\MyModule;
use Aero\AccountArea\Http\Responses\AccountOrdersPage;
use Aero\Common\Providers\ModuleServiceProvider;
class ServiceProvider extends ModuleServiceProvider
{
   public function setup()
   {
       $this->loadViewsFrom(__DIR__.'/../resources/views', 'my-module');
       AccountOrdersPage::extend(function (AccountOrdersPage $page) {
           $page->addSection('my-section', 'my-module::view');
           $page->addSectionAfter('my-other-section', 'my-module::view', 'navbar');
       });
   }
}

How do I add a custom CSS file to the account area?
You can add a custom CSS file to the account area by creating a file named 
account-area.cssin your public directory.
Example contents where we're overriding the default colours:
:root {
    --bpa-color-primary: deepskyblue;
    --bpa-color-success: forestgreen;
    --bpa-color-warning: darkorange;
    --bpa-color-error: red;
    --bpa-color-helpbox: aliceblue;
    --bpa-color-content-background: ghostwhite;
}

How do I create a new form for the account area?
To create a form you need to create a class that extends the 
AccountAreaFormclass.
The 
$sectionsarray lists the views used to render the form.
The 
methodmethod defines the method that the form will use (this should be 
post, 
get, 
put, or 
delete).
The 
routemethod should return the URL for the form. A 
$datavariable is accessible in case the route requires parameters that are available in the data.
<?php
namespace Acme\MyModule;
use Aero\AccountArea\AccountAreaForm;
class MyForm extends AccountAreaForm
{
    protected $sections = [
        'email' => 'account-area::partials.email-input',
        'password' => 'account-area::partials.password-input',
        'forgot-password' => 'account-area::partials.login-forgot-password-link',
        'submit' => 'account-area::partials.login-button',
    ];
    protected function method(): string
    {
        return 'post';
    }
    protected function route($data): string
    {
        return route('login');
    }
}
Registering a form
Any form you wish to use in the account area should be registered in your service provider.
<?php
namespace Acme\MyModule;
use Aero\AccountArea\AccountArea;
use Aero\Common\Providers\ModuleServiceProvider;
class ServiceProvider extends ModuleServiceProvider
{
    public function setup(): void 
    {
        AccountArea::registerForm(MyForm::class);
    }
}

What are the available slots in the account area?
The following slots can be injected into.
orders.order.card.header- 
orders.order.card.dispatched- 
orders.order.card.not-dispatched- 
orders.order.card.footer- 
addresses.address.card.footer

Admin Dashboard Lenses
Admin Dashboard Lenses

What are the default dashboard lenses available?
The default dashboard lenses are stored in the admin configuration file as an array called dashboard_lenses. You can add or remove dashboard lens classes from this array.
Key
Class
Permission
revenue-lens
Aero\Admin\Lenses\RevenueLens
reports.orders
average-order-value-lens
Aero\Admin\Lenses\AverageOrderValueLens
reports.orders
top-shipping-countries-lens
Aero\Admin\Lenses\TopShippingCountriesLens
reports.orders
returns-rate-lens
Aero\Admin\Lenses\ReturnsRateLens
reports.orders
orders-lens
Aero\Admin\Lenses\OrdersLens
reports.orders
top-selling-manufacturers-lens
Aero\Admin\Lenses\TopSellingManufacturersLens
reports.catalog
top-selling-items-lens
Aero\Admin\Lenses\TopSellingItemsLens
reports.catalog

What is a dashboard lens?
Dashboard lenses are the blocks of content that make up the interactive part of the admin dashboard.
They provide an overview of various stats such as your total revenue, orders, and your top selling products.
There are a number of default lenses and you can also create your own

How do I create a custom dashboard lens?
To create a dashboard lens you need to create a class that extends 
Aero\Admin\AdminLensand implements the datamethod.
data Method
This method is executed every time the lens needs data (when the dashboard loads or when the dashboard date range is changed). This method must return an Illuminate\Http\JsonResponsebecause it is called on the frontend by javascript.
data Method Parameters
Type
Description
Request
The first parameter is the request.
Array
The second parameter is an array that holds the start and end date selected for the dashboard stats. This array has a start and end key that has the dates.
Array
The third parameter is an array that holds information about the comparison dates. This array has a type key that is 1 for the same period prior, 2 for the same period 1 year ago, or 3 for a custom period. This array has a date key that has the same structure as the second parameter. This array also has a text key that holds text to be displayed to explain the comparison.
Here is the code for our top shipping countries implementation:
<?php
namespace Aero\Admin\Lenses;
use Aero\Admin\AdminLens;
use Aero\Cart\Models\Order;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Str;
class TopShippingCountriesLens extends AdminLens
{
   protected $title = 'Top Shipping Countries';
   protected static $permission = 'reports.orders';
   protected static $containerClass = 'row-span-2';
   protected $view = 'admin::lenses.percentage-list';
   public function data(Request $request, array $date, array $compare): JsonResponse
   {
       $countries = Order::visible()->with('shippingAddress.country')->whereHas('shippingAddress')
           ->whereBetween('ordered_at', [$date['start'], $date['end']])->get()
           ->groupBy('shippingAddress.country.name')
           ->map(function ($group, $key) {
               return [
                   'name' => $key,
                   'count' => $group->count(),
                   'percentage' => 0,
               ];
           })->sortByDesc('count')->take(5);
       $total = $countries->reduce(function ($count, $country) {
           return $count + $country['count'];
       }, 0);
       $countries->transform(function ($country) use ($total) {
           $country['value'] = number_format(($country['count'] / $total) * 100, 2);
           $country['text'] = $country['count'].' '.Str::plural('order', $country['count']);
           $country['percentage'] = $country['value'].'%';
           return $country;
       });
       return response()->json([
           'items' => $countries,
       ]);
   }
}

How do I use a custom view for my dashboard lens?
You can optionally make your dashboard lens use a custom view. To do this you need to set the protected 
$viewstring on your lens to the view you would like to use.
<?php
namespace Aero\Admin\Lenses;
use Aero\Admin\AdminLens;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
class TopShippingCountriesLens extends AdminLens
{
   protected $view = 'admin::lenses.percentage-list';
   public function data(Request $request, array $date, array $compare): JsonResponse
   {
       return response()->json([]);
   }
}
Your view will have access to Blade and Vue as it is rendered as a slot. You will have access to the following variables:
Variable
Type
Description
lens.data
Vue
This is an object that holds the response from the data method of your admin lens. You can effectively pass any data you want in the json response and have access to it in Vue through this variable.
lens.date
Vue
This is an object that has start, end and compare keys.
lens.loading
Vue
This is a boolean that is true while the lens is making an API call to get its data.
lens.notLoading
Vue
This is a boolean that is false unless the lens is making an API call.
lens.noData
Vue
This is a boolean that is true if the store has no orders.
lens.hasData
Vue
This is a boolean that is true if the store has orders.
lens.renderDateGetVariables
Vue
This returns an empty string or the selected date formatted as the $dateGetVariables property on the lens class describes.
$title
Blade
This is the value of the $title property on your lens class.
$link
Blade
This is the value of the link() function on your lens class.
$noTextData
Blade
This is the value of the $noDataText property on your lens class.
Here is the code for our default lens view:
<div class="card h-full relative">
   <svg class="w-4 h-4 absolute pin-t pin-r m-4 text-grey-light stroke-current [ animation-spin ]" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 18.111 18.068" v-if="lens.loading">
       <path d="M20,4V9h-.582M4.062,11A8,8,0,0,1,19.418,9m0,0H15M4,20V15h.581m0,0a8,8,0,0,0,15.357-2M4.581,15H9" transform="translate(-2.945 -2.964)" fill="none" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
   </svg>
   <svg class="w-4 h-4 absolute pin-t pin-r m-4 text-success stroke-current [ animation-fade-out ]" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 18.11 18.07" v-else>
       <g fill="none" stroke-linecap="round" stroke-linejoin="round" stroke-width="2">
           <path d="M2.06 10.03l4 4 10-10" data-name="Path 103"/>
       </g>
   </svg>
   <h3 class="uppercase text-base font-normal text-text h-auto p-0 mb-3">{{ $title }}</h3>
   <div class="flex flex-wrap -mx-3">
       <div class="w-1/2 px-3 mb-6" v-if="lens.loading || lens.noData">
           <div class="mb-1">
               <template v-if="lens.loading">
                   <skeleton-box height="2.125rem" width="4rem" />
               </template>
               <template v-else-if="lens.noData">
                   <p class="text-3xl font-medium mb-1">–</p>
               </template>
           </div>
           <template v-if="lens.loading">
               <skeleton-box width="9rem" height="0.8125rem" />
           </template>
           <template v-else-if="lens.noData">
               <p class="text-xs">{{ $noDataText }}</p>
           </template>
       </div>
       <div class="w-1/2 px-3 mb-6" v-if="lens.notLoading && lens.hasData" v-for="data in lens.data" :key="data.currency_code">
           <div class="mb-1">
               <p class="text-3xl font-medium" v-html="data.text"></p>
           </div>
           <p class="text-xs"><strong v-text="data.compare.percentage" class="font-medium" :class="{ 'text-success' : data.compare.up, 'text-error' : ! data.compare.up }"></strong> <span v-text="data.compare.text"></span></p>
       </div>
   </div>
   @isset($link)
       <a class="dashboard-link" v-if="lens.noData">{{ $link['text'] }}</a>
       <a class="dashboard-link" :href="'{{ $link['route'] }}' + lens.renderDateGetVariables" v-else>{{ $link['text'] }}</a>
   @endif
</div>
Adding Permissions to your Dashboard Lens
You can optionally make your dashboard lens require a permission (if the user doesn’t have the permission then the lens will not be rendered). To do this you need to set the protected static $permission string on your lens class to the permission you would like to use.
<?php
namespace Aero\Admin\Lenses;
use Aero\Admin\AdminLens;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
class TopShippingCountriesLens extends AdminLens
{
   protected static $permission = 'dashboard.lens.orders';
   public function data(Request $request, array $date, array $compare): JsonResponse
   {
       return response()->json([]);
   }
}

How do I add a custom dashboard lens to the dashboard?
To add your custom dashboard lens to the dashboard you need to register it with the admin and the admin dashboard.
To register your dashboard lens with the admin you need to call the Aero\Admin\Facades\Adminfacade registerLensmethod and pass in your dashboard lens class.
<?php
namespace Acme\MyModule;
use Aero\Admin\Facades\Admin;
use Aero\Admin\Lenses\RevenueLens;
use Aero\Common\Providers\ModuleServiceProvider;
class ServiceProvider extends ModuleServiceProvider
{
   public function setup()
   {
       Admin::registerLens(RevenueLens::class);
   }
}
Once you have registered your dashboard lens with the admin you are able to extend Aero\Admin\Http\Responses\AdminDashboardPageand add your lens using the setLens, addLens, addLensBefore, or addLensAftermethods.
setLens Method
This method accepts a key and a lens class. The key's current value will be replaced with the lens class you provide.
addLens Method
This method accepts a key and a lens class. When using this method your lens will be added as the first dashboard lens.
addLensBefore Method
This method accepts the same parameters as the addLensmethod but additionally accepts a third parameter, the key of the lens this new lens should be added before.
addLensAfter Method
This method accepts the same parameters as the addLensmethod but additionally accepts a third parameter, the key of the lens this new lens should be added after.
Finding the Current Dashboard Lenses Keys
If you do not know the keys of the current dashboard lenses you can use this code snippet to dump the current keys out when visiting the dashboard.
<?php
namespace Acme\MyModule;
use Aero\Admin\Http\Responses\AdminDashboardPage;
use Aero\Common\Providers\ModuleServiceProvider;
class ServiceProvider extends ModuleServiceProvider
{
   public function setup()
   {
       AdminDashboardPage::extend(function (AdminDashboardPage $page) {
           dd($page->getLenses()->keys());
       });
   }
}
<?php
namespace Acme\MyModule;
use Aero\Admin\Facades\Admin;
use Aero\Admin\Http\Responses\AdminDashboardPage;
use Aero\Admin\Lenses\RevenueLens;
use Aero\Common\Providers\ModuleServiceProvider;
class ServiceProvider extends ModuleServiceProvider
{
   public function setup()
   {
       Admin::registerLens(RevenueLens::class);
       AdminDashboardPage::extend(function (AdminDashboardPage $page) {
           $page->addLensBefore('revenue-lens', RevenueLens::class, 'average-order-value-lens');
       });
   }
}

How do I remove a dashboard lens from the dashboard?
You are able to extend Aero\Admin\Http\Responses\AdminDashboardPageand use the removeLensmethod. You will need to pass in the key of the lens that you want to remove.
<?php
namespace Acme\MyModule;
use Aero\Admin\Http\Responses\AdminDashboardPage;
use Aero\Admin\Lenses\RevenueLens;
use Aero\Common\Providers\ModuleServiceProvider;
class ServiceProvider extends ModuleServiceProvider
{
   public function setup()
   {
       AdminDashboardPage::extend(function (AdminDashboardPage $page) {
           $page->removeLens('revenue-lens');
       });
   }
}

Admin Filters
Admin Filters

What is an admin filter?
Admin filters are the filters that populate the sidebars of reports and resource lists. As the name suggests, they allow users to filter the data they’re seeing through the report or resource list.
All admin filters ultimately extend Aero\Admin\Filters\AdminFilterand therefore all have these common methods that you can optionally override:
title Method
This is a public method that returns the title for the filter. This title is displayed on the frontend. By default the title is generated from the class name.
key Method
This is a public method that returns the key for the filter. The key is used for the request parameter for the filter. By default the key is generated from the class name.
You can read about the other methods in the "How do I create a custom Admin Filter?

How do I create a custom admin filter?
To create a custom admin filter you need to create a class that extends Aero\Admin\Filters\AdminFilterand implements the handle method.
handle Method
This method is executed on every request made to a report or resource list with your filter and accepts 2 parameters. The first parameter is a Symfony\Component\HttpFoundation\ParameterBag, and the second parameter is the query.
render Method
This method defines how the filter is rendered. By default it will return a view (that you can set with the protected $viewproperty) and pass in the **options()**and **viewData()**arrays.
stateFields Method
This method returns an array of names for the parameters this filter uses in the request. It’s important that if you use any parameters outside of the default parameter (which uses the **$this->key()**for the name) that you override this method and return them in the array. This method is used to gather all of the fields that should be reset to remove your filter.
options Method
This method returns an array of data that is passed to the view by the render method. If you’re creating a new filter that may be used by many classes it is a good idea to use this method instead of the viewData method so that classes that extend your class can use the viewData method if required.
viewData Method
This method returns an array of data that is passed to the view by the render method.
Here is the code for our date range admin filter implementation:
<?php
namespace Aero\Admin\Filters;
use Symfony\Component\HttpFoundation\ParameterBag;
abstract class DropdownAdminFilter extends AdminFilter
{
   protected $view = AdminFilterTypes::DROPDOWN;
   protected function selectedOption()
   {
       return request()->input($this->key());
   }
   protected function options(): array
   {
       return [
           'title' => $this->title(),
           'key' => $this->key(),
           'options' => $this->dropdowns(),
           'selected' => $this->selectedOption(),
       ];
   }
   public function handle(ParameterBag $parameters, $query)
   {
       if (($selected = $parameters->get($this->key())) && $selected != '') {
           $this->handleDropdown($selected, $query);
       }
   }
   abstract protected function handleDropdown($selected, $query);
   abstract protected function dropdowns(): array;
}

How do I create a checkbox list admin filter?
To create a checkbox list admin filter you need to create a class that extends Aero\Admin\Filters\CheckboxListAdminFilterand implements the handleCheckboxListand checkboxesmethods.
handleCheckboxList Method
This method is executed when any checkboxes are checked and accepts two parameters. The first parameter is an array of the selected items and the second parameter is the query.
checkboxes Method
This method provides the results shown in the rendered checkbox list on the frontend. It needs to return an array that has id, name, and url as keys. The url key should use the **$this->getUrlFor()**helper method to create a url for the id.
<?php
namespace Acme\MyModule\Filters;
use Aero\Admin\Filters\CheckboxListAdminFilter;
use Aero\Cart\Models\OrderStatus;
class OrderStatusStateAdminFilter extends CheckboxListAdminFilter
{
   protected function handleCheckboxList(array $selected, $query)
   {
       $query->whereIn('state', $selected);
   }
   protected function checkboxes(): array
   {
       return OrderStatus::query()->distinct('state')->get()->map(function ($status) {
           return [
               'id' => $status->state,
               'name' => ucwords(implode(' ', explode('_', $status->state))),
               'url' => $this->getUrlFor($status->state),
           ];
       })->toArray();
   }
}

How do I create an options date range admin filter?
The options date range admin filter works in the same way as the date range admin filter but displays some dynamic options to the user on the frontend. These dynamic options are especially useful when users save their applied filters.
To create an options date range admin filter you need to create a class that extends Aero\Admin\Filters\OptionsDateRangeAdminFilterand implements the handleDateRangemethod.
handleDateRange Method
This method is executed when a date is set and accepts 3 parameters. The first parameter is a Carbon object for the selected start date, the second parameter is a Carbon object for the selected end date, and the third parameter is the query.
You can switch this filter to not use past options (such as yesterday, last 7 days, last 30 days) by setting the $lastModeproperty to false. If this property is false the options will become future options (such as tomorrow, next 7 days, next 30 days).
<?php
namespace Aero\Admin\Filters\Order;
use Aero\Admin\Filters\OptionsDateRangeAdminFilter;
class OrderDeliverOnDateAdminFilter extends OptionsDateRangeAdminFilter
{
   protected $lastMode = false;
   protected function handleDateRange($startDate, $endDate, $query)
   {
       $query->whereBetween('deliver_on', [$startDate, $endDate]);
   }
}

How do I create a dropdown admin filter?
To create an options date range admin filter you need to create a class that extends Aero\Admin\Filters\DropdownAdminFilter and implements the handleDropdown and dropdowns methods.
handleDropdown Method
This method is executed when a dropdown option is set and accepts 2 parameters. The first parameter is the value selected and the second parameter is the query.
dropdowns Method
This method provides the results shown in the rendered dropdown on the frontend. It needs to return an array that has name and value as keys. If you use an empty string for a value then your handleDropdown method will not be called when that option is selected. This is useful for allowing users to effectively turn your filter off.
<?php
namespace Aero\Admin\Filters\MailNotification;
use Aero\Admin\Filters\DropdownAdminFilter;
class MailNotificationLayoutAdminFilter extends DropdownAdminFilter
{
   protected function handleDropdown($selected, $query)
   {
       switch ($selected) {
           case 'system':
               $query->where('layout', 'system');
               break;
           case 'customer':
               $query->where('layout', 'customer');
               break;
       }
   }
   protected function dropdowns(): array
   {
       return [
           [
               'value' => '',
               'name' => 'View All',
           ],
           [
               'value' => 'customer',
               'name' => 'Customer',
           ],
           [
               'value' => 'system',
               'name' => 'System',
           ],
       ];
   }
}

How do I create a date range admin filter?
To create a date range admin filter you need to create a class that extends Aero\Admin\Filters\DateRangeAdminFilterand implements the handleDateRangemethod.
handleDateRange Method
This method is executed when a date is set and accepts 3 parameters. The first parameter is a Carbon object for the selected start date, the second parameter is a Carbon object for the selected end date, and the third parameter is the query.
<?php
namespace Acme\MyModule\Filters;
use Aero\Admin\Filters\DateRangeAdminFilter;
class CreatedAtAdminFilter extends DateRangeAdminFilter
{
   protected function handleDateRange($startDate, $endDate, $query)
   {
       $query->whereBetween('created_at', [$startDate, $endDate]);
   }
}

How do I create a searchable select admin filter?
To create an options date range admin filter you need to create a class that extends Aero\Admin\Filters\SearchableSelectAdminFilter and implements the handleSearchableSelect, model, modelName, and searchRoute methods.
handleSearchableSelect Method
This method is executed when an option has been selected and accepts 2 parameters. The first parameter is an array of selected values and the second parameter is the query.
model Method
This method needs to return an instance of the model that will be used for searching. This will be used to automatically fetch the selected data.
modelName Method
This method defines the name that should be used in the searchable select for the selected options.
searchRoute Method
This method defines the route that will be used by the searchable select component to fetch data.
<?php
namespace Aero\Admin\Filters\Order;
use Aero\Admin\Filters\SearchableSelectAdminFilter;
use Aero\Cart\Models\Discount;
use Illuminate\Database\Eloquent\Model;
class OrderDiscountAdminFilter extends SearchableSelectAdminFilter
{
   protected $multiple = true;
   protected function handleSearchableSelect(array $selected, $query)
   {
       $query->where(function ($query) use ($selected) {
           $query->whereHas('discounts', static function ($query) use ($selected) {
               $query->whereIn('id', $selected);
           });
       });
   }
   protected function model(): Model
   {
       return new Discount();
   }
   protected function modelName(Model $model)
   {
       return $model->code ?? ($model->name ?? 'Discount #'.$model->id);
   }
   protected function searchRoute(): string
   {
       return route('admin.discounts.search');
   }
}

Admin Product Picker Component
Admin Product Picker Component

How do I use the product picker in my custom module?
The Product Picker provides a modal that lets users select a single or many products/variants with quantities. Parts of the admin use this such as the Price Lists and Create Order form. Modules such as aerocargo/upsellsalso make use of the Product Picker.
You can use the 
<product-picker></product-picker>Vue component from any admin view inside of your module that extends the official admin layout (
admin::layouts.main). When using the Product Picker you need to provide a url for the get-products-url prop (this value is usually always the 
admin.catalog.products.pickerroute) and you need to provide a slot for the button that will be used to open the picker.
@extends('admin::layouts.main')
@section('content')
   <h2>
       <a href="{{ route('admin.modules') }}" class="btn">@include('admin::icons.back') Back</a>
       <span class="ml-4">My Module</span>
   </h2>
   <div class="card">
       <product-picker v-slot="picker"
                       get-products-url="{{ route('admin.catalog.products.picker') }}">
           <a href="#" @click.prevent="picker.open" class="btn">Add Product</a>
       </product-picker>
   </div>
@endsection

What are the available props for the product picker?
Prop
Type
Description
get-products-url
String
This is the only required prop. This is the URL for the API to get the products from. It’s common to set this value to {{ route('admin.catalog.products.picker') }}.
image-url-prefix
String
This is the image factory prefix to use for the product image URLs. This is set by default to use {{ image_factory(60, 60)->contain() }}.
no-image-url
String
This is the image URL to use when a product doesn’t have an image. This is set by default to use {{ asset('modules/aerocommerce/admin/no-image.svg') }}.
action-name
String
This is the button text shown on the confirm button of the product picker modal (bottom right). This is set by default to “Add Products”.
clear-selected-after-emit
Boolean
pick-product
Boolean
This is a boolean that defaults to false. When true, products will be allowed to be selected as well as variants.
product-only
Boolean
This is a boolean that defaults to false. When true, variants will not be listed and not selectable.
pick-only
Boolean
This is a boolean that defaults to false. When true, variants will not need to be in stock to be selectable.
needs-quantity
Boolean
This is a boolean that defaults to true. When false, a quantity will not be able to be set for the selected variants/products.
emit-image
Boolean
This is a boolean that defaults to false. When true, the products/variants image will be emitted when selected.
emit-sku
Boolean
This is a boolean that defaults to false. When true, the products/variants sku will be emitted when selected.
emit-model
Boolean
This is a boolean that defaults to false. When true, the products/variants model will be emitted when selected.
emit-name
Boolean
This is a boolean that defaults to false. When true, the products/variants name will be emitted when selected.
emit-price
Boolean
This is a boolean that defaults to false. When true, the products/variants price will be emitted when selected.
Example
You can make use of the props like you usually would with a Vue component
@extends('admin::layouts.main')
@section('content')
   <h2>
       <a href="{{ route('admin.modules') }}" class="btn">@include('admin::icons.back') Back</a>
       <span class="ml-4">My Module</span>
   </h2>
   <div class="card">
       <product-picker v-slot="picker"
                       get-products-url="{{ route('admin.catalog.products.picker') }}"
                       :max-selection="5"
                       :pick-product="true"
                       :emit-name="true"
                       :emit-model="true"
                       :emit-sku="true">
           <a href="#" @click.prevent="picker.open" class="btn">Add Product</a>
       </product-picker>
   </div>
@endsection

What are the available events for the product picker?
Event
Description
products-selected
This event emits when the action button is clicked (bottom right of the modal) and provides an array of all of the selected product(s)/variant(s).
selected
This event emits when the action button is clicked (bottom right of the modal) and is emitted for each product/variant that has been selected - only a single product/variant is provided.
Example
The usual use-case for the Product Picker is to allow users to select products and/or variants to be added to a list. This example code is a Blade view that allows the user to select up to 5 products or variants and then displays them in a list with a custom message. It also allows the user to remove the selected products/variants. The next steps to make this code fully functional would be to wrap the table in a form and to add a submit button that saves the products/variants.
@extends('admin::layouts.main')
@section('content')
   <h2>
       <a href="{{ route('admin.modules') }}" class="btn">@include('admin::icons.back') Back</a>
       <span class="ml-4">My Module</span>
   </h2>
   <div class="card p-0">
       <table>
           <thead>
               <tr class="header">
                   <th class="whitespace-no-wrap">Model</th>
                   <th class="whitespace-no-wrap">Sku</th>
                   <th class="whitespace-no-wrap">Message</th>
                   <th></th>
               </tr>
           </thead>
           <tbody>
               <tr v-for="(item, index) in list" :key="item.buyable_id + item.buyable_type" v-cloak>
                   <td class="whitespace-no-wrap">
                       <input type="hidden" :name="'items[' + index + '][buyable_id]'" :value="item.buyable_id">
                       <input type="hidden" :name="'items[' + index + '][buyable_type]'" :value="item.buyable_type">
                       <span v-text="item.model"></span>
                   </td>
                   <td class="whitespace-no-wrap" v-if="item.sku" v-text="item.sku"></td>
                   <td class="whitespace-no-wrap" v-else><span class="text-grey">—</span></td>
                   <td class="whitespace-no-wrap">
                       <input type="text" :name="'items[' + index + '][message]'" v-model="item.message" placeholder="Message">
                   </td>
                   <td>
                       <div class="flex items-center justify-end">
                           <a href="#" @click.prevent="list.splice(index, 1)">@include('admin::icons.bin')</a>
                       </div>
                   </td>
               </tr>
               <tr v-if="list.length === 0">
                   <td colspan="4">No data to show</td>
               </tr>
           </tbody>
       </table>
       <product-picker v-slot="picker"
                       v-if="list.length < 5"
                       get-products-url="{{ route('admin.catalog.products.picker') }}"
                       :max-selection="5"
                       :pick-product="true"
                       :emit-name="true"
                       :emit-model="true"
                       :emit-sku="true"
                       @selected="selected">
           <a href="#" @click.prevent="picker.open" class="btn m-4">Add Product</a>
       </product-picker>
   </div>
@endsection
@push('scripts')
   <script>
       window.AeroAdmin.addData('list', []);
       window.AeroAdmin.addMethod('selected', function (item) {
           if (this.findIndex(item.buyable_id, item.buyable_type) === -1) {
               this.list.push({
                   ...item,
                   message: '',
               });
           } else {
               this.$notify({
                   type: 'error',
                   title: (item.sku ?? item.name) + ' has already been added to the list',
               });
           }
       });
       window.AeroAdmin.addMethod('findIndex', function (id, type) {
           return this.list.findIndex((l) => l.buyable_id === id && l.buyable_type === type);
       });
   </script>
@endpush

Admin Reports
Admin Reports

How do I create a custom report?
To create and register a custom report you need to use the Aero\Admin\AdminReportfacade inside of your app service providers boot method or module service providers setup method.
The facade requires a class that extends Aero\Admin\Reports\Report. Additionally, you can provide a title and summary (shown in the reports list in the admin) and any permissions required to use the report.
<?php
namespace Acme\MyModule;
use Acme\MyModule\Reports\OrdersReport;
use Aero\Admin\AdminReport;
use Aero\Common\Providers\ModuleServiceProvider;
class ServiceProvider extends ModuleServiceProvider
{
   public function setup()
   {
       AdminReport::create(OrdersReport::class)
           ->title('Simple Orders Report')
           ->summary('View a simple report for your orders')
           ->permissions('orders.view');
   }
}
Your report class must implement a newQuerymethod that must return a Illuminate\Database\Eloquent\Builder. This method provides the query your report will run from.
<?php
namespace Acme\MyModule\Reports;
use Aero\Admin\Reports\Report;
use Aero\Cart\Models\Order;
use Illuminate\Database\Eloquent\Builder;
class OrdersReport extends Report
{
   protected function newQuery(): Builder
   {
       return Order::with([
           'status', 'items', 'currency', 'discounts', 'payments.method',
       ]);
   }
}

How do I add lenses to my custom report?
You need to create a class for your lens that extends Aero\Admin\Reports\ReportLensand implements the required public value function. The value function will return a string used to display your lens.
<?php
namespace Acme\MyModule\Reports\Lenses;
use Aero\Admin\Reports\ReportLens;
class TotalOrdersLens extends ReportLens
{
   protected function value()
   {
       return $this->query->count();
   }
}
You can optionally override the rendermethod to change the view that your lens uses. The default rendermethod:
public function render()
{
   return view('admin::reports.lens', ['title' => $this->title(), 'content' => $this->value()]);
}
You can also additionally override the titlemethod to change the title shown on the frontend. By default this method returns a title that is generated from the lens class name.
Registering a Lens with your Report
To register your newly created lens with your report you need to ensure the report uses the Aero\Admin\Reports\Traits\HasLenstrait and that your lens class is in the reports protected static $lenses array.
<?php
namespace Acme\MyModule\Reports;
use Acme\MyModule\Reports\Lenses\TotalOrdersLens;
use Aero\Admin\Reports\Report;
use Aero\Admin\Reports\Traits\HasLens;
use Aero\Cart\Models\Order;
use Illuminate\Database\Eloquent\Builder;
class OrdersReport extends Report
{
   use HasLens;
   protected function newQuery(): Builder
   {
       return Order::with([
           'status', 'items', 'currency', 'discounts', 'payments.method',
       ]);
   }
   protected static $lenses = [
       TotalOrdersLens::class,
   ];
}

How do I add a table to my custom report?
You need to create a class for your table that extends Aero\Admin\Reports\ReportTableand implements the required public columns function. This columns function will be used to return an array containing your tables columns.
<?php
namespace Acme\MyModule\Reports\Tables;
use Aero\Admin\Reports\ReportTable;
class OrdersReportTable extends ReportTable
{
   public function columns(): array
   {
       return [
       ];
   }
}
Registering a Table with your Report
To register your newly created table with your report you need to ensure the report uses the Aero\Admin\Reports\Traits\HasTablestrait and that your table class is in the reports protected static $tables array.
<?php
namespace Acme\MyModule\Reports;
use Acme\MyModule\Reports\Tables\OrdersReportTable;
use Aero\Admin\Reports\Report;
use Aero\Admin\Reports\Traits\HasTables;
use Aero\Cart\Models\Order;
use Illuminate\Database\Eloquent\Builder;
class OrdersReport extends Report
{
   use HasTables;
   protected function newQuery(): Builder
   {
       return Order::with([
           'status', 'items', 'currency', 'discounts', 'payments.method',
       ]);
   }
   protected static $tables = [
       OrdersReportTable::class,
   ];
}
Adding Columns to your Table
To add columns to your table you need to use the Aero\Admin\Reports\ReportTableColumnfacade in your tables columns array method.
ReportTableColumn::create Parameters
Type
Description
String
The first parameter is the header of the column.
Closure
The second parameter is a closure that handles the display content for the column. The closure accepts a $row variable and returns a string or view value.
String
Null
<?php
namespace Acme\MyModule\Reports\Tables;
use Aero\Admin\Reports\ReportTable;
use Aero\Admin\Reports\ReportTableColumn;
class OrdersReportTable extends ReportTable
{
   public function columns(): array
   {
       return [
           ReportTableColumn::create('Date', function ($row) {
               if ($row->ordered_at) {
                   return $row->ordered_at->format('D jS M, H:i');
               }
               return view('admin::reports.partials.muted-created-at', compact('row'));
           }),
           ReportTableColumn::create('Order', function ($row) {
               return $row->reference;
           }),
       ];
   }
}
Making your Column Searchable
You can use the setSearchActionmethod to define how your column is searched.
Type
Description
Closure
The first parameter is a closure that is called when your column is being used for a search. The closure is passed the query and search term.
String
Null
ReportTableColumn::create('Order', function ($row) {
   return $row->reference;
})
->setSearchAction(function ($query, $term) {
   $term = ltrim($term, '#');
   $query->whereLower('reference', 'like', "%{$term}%");
}, 'Order Reference')
Making your Column Not Exportable
To make your column not exportable you can use the notExportablemethod.
ReportTableColumn::create('Order', function ($row) {
   return $row->reference;
})
->notExportable()
Setting your Columns Export Content
You can use the setExportContentmethod to define how your column should return data when exporting. This is useful for times where instead of returning a view on the Aero Admin you want your column to return raw data.
The setExportContentmethod accepts a closure that is passed the row.
ReportTableColumn::create('Date', function ($row) {
   if ($row->ordered_at) {
       return $row->ordered_at->format('D jS M, H:i');
   }
   return view('admin::reports.partials.muted-created-at', compact('row'));
})
->setExportContent(function ($row) {
   if ($row->ordered_at) {
       return $row->ordered_at->format('D jS M, H:i');
   }
   return $row->created_at->format('D jS M, H:i');
})
Making your Column Invisible
You can use the invisible method to make your column invisible. Do note that your column will still be visible in an export.
ReportTableColumn::create('Order', function ($row) {
   return $row->reference;
})
->invisible()
Setting your Columns Position
Columns are ordered by an Integer position value (lowest numbers go first). You can set a column's position using the position method. On the default Aero reports we increment each rows column.
ReportTableColumn::create('Order', function ($row) {
   return $row->reference;
})
->position(1)

How do I add exporters to my custom report?
To add an exporter to your report you need to ensure the report uses the Aero\Admin\Reports\Traits\CanExporttrait and that the exporter class is in the reports protected static $exports array.
By default Aero provides a CSV, XML, and JSON exporter.
<?php
namespace Acme\MyModule\Reports;
use Aero\Admin\Reports\Exporters\CSVExporter;
use Aero\Admin\Reports\Exporters\JSONExporter;
use Aero\Admin\Reports\Exporters\XMLExporter;
use Aero\Admin\Reports\Report;
use Aero\Admin\Reports\Traits\CanExport;
use Aero\Cart\Models\Order;
use Illuminate\Database\Eloquent\Builder;
class OrdersReport extends Report
{
   use CanExport;
   protected function newQuery(): Builder
   {
       return Order::with([
           'status', 'items', 'currency', 'discounts', 'payments.method',
       ]);
   }
   protected static $exports = [
       CSVExporter::class,
       XMLExporter::class,
       JSONExporter::class,
   ];
}
Creating a Report Exporter
You need to create a class for your exporter that extends Aero\Admin\Reports\ReportExporterand implements the required public handle function.
Handle Parameters
Type
Description
Collection
The first parameter is a collection of the exportable tables columns.
Builder
The second parameter is the query that should be used to get the data for the export.
String
The third parameter is the key of the table being exported.
<?php
namespace Acme\MyModule\Reports\Exporters;
use Aero\Admin\Reports\ReportExporter;
use Carbon\Carbon;
class CustomExporter extends ReportExporter
{
   public function handle($columns, $rows, $key)
   {
       $file = $key.'-'.Carbon::now().'.csv';
       return response()->streamDownload(function () use ($columns, $rows) {
           $file = fopen('php://output', 'wb');
           fputcsv($file, $columns->map->header()->toArray());
           $rows->chunk(100, function ($chunk) use ($file, $columns) {
               foreach ($chunk as $row) {
                   $rowData = [];
                   foreach ($columns as $column) {
                       $rowData[] = $column->exportContent($row);
                   }
                   fputcsv($file, $rowData);
               }
           });
           fclose($file);
       }, $file,
           ReportExporter::getHttpHeaders('text/csv', $file)
       );
   }
}
Now you need to add your newly created exporter to your report as shown at the start of this article.

How do I extend existing reports?
Reports can be extended using a pipeline from your modules service providers setup method or your projects app service providers boot method. You can read more about pipelines here (link).
Adding a Column
To add columns to a table you can use the addColumn, addColumnBefore, or addColumnAftermethods from within the pipeline. These methods return a usual report table column which allows you to chain on the other available column methods.
addColumn Method
This method accepts a column header and a closure that defines how the column will be rendered. A column added with this method will be added as the first column.
addColumnBefore Method
This method accepts the same parameters as the addColumnmethod but additionally accepts a third parameter, the key of the column this new column should be added before.
addColumnAfter Method
This method accepts the same parameters as the addColumnmethod but additionally accepts a third parameter, the key of the column this new column should be added after.
Finding the Current Column Keys
If you do not know the keys of the current columns you can use this code snippet to dump the current keys out when visiting the report.
<?php
namespace Acme\MyModule;
use Aero\Admin\Reports\Implementations\SalesReport;
use Aero\Common\Providers\ModuleServiceProvider;
class ServiceProvider extends ModuleServiceProvider
{
   public function setup()
   {
       SalesReport::extend(function (SalesReport $report) {
           dd($report->table()->getColumns()->map->key());
       });
   }
}
<?php
namespace Acme\MyModule;
use Aero\Admin\Reports\Implementations\SalesReport;
use Aero\Common\Providers\ModuleServiceProvider;
class ServiceProvider extends ModuleServiceProvider
{
   public function setup()
   {
       SalesReport::extend(function (SalesReport $report) {
           $report->table()->addColumn('My Column', function ($row) {
               return 'example';
           })
           ->setExportContent(function ($row) {
               return 'export content';
           });
           $report->table()->addColumnBefore('Before SKU', function ($row) {
               return $row->sku;
           }, 'sku');
           $report->table()->addColumnAfter('After Sku', function ($row) {
               return $row->sku;
           }, 'sku');
       });
   }
}
Adding a Lens
To add a lens to a report you can use the addLensmethod from within the pipeline. This method expects you to pass a report lens class. You can create a report lens class as shown in the article "How do I add Lenses to my custom Report?
<?php
namespace Acme\MyModule;
use Acme\MyModule\Reports\Lenses\TotalRevenueLens;
use Aero\Admin\Reports\Implementations\SalesReport;
use Aero\Common\Providers\ModuleServiceProvider;
class ServiceProvider extends ModuleServiceProvider
{
   public function setup()
   {
       SalesReport::extend(function (SalesReport $report) {
           $report->addLens(TotalRevenueLens::class);
       });
   }
}
Adding a Filter
To add a filter to a report you can use the addFilter method from within the pipeline. This method expects you to pass an admin filter class. You can create an admin filter class as shown in the article "How do I add Admin Filters to my custom Report?
<?php
namespace Acme\MyModule;
use Aero\Admin\Filters\Order\OrderStatusAdminFilter;
use Aero\Admin\Reports\Implementations\OrderBreakdownReport;
use Aero\Common\Providers\ModuleServiceProvider;
class ServiceProvider extends ModuleServiceProvider
{
   public function setup()
   {
       OrderBreakdownReport::extend(function (OrderBreakdownReport $report) {
           $report->addFilter(OrderStatusAdminFilter::class);
       });
   }
}

How do I add admin filters to my custom report?
You need to create an Admin Filter class as shown in the article "How do I create a custom report?
To register your newly created admin filter with your report you need to ensure the report uses the Aero\Admin\Reports\Traits\HasFilterstrait and that your admin filter class is in the reports protected static $filters array.
<?php
namespace Acme\MyModule\Reports;
use Aero\Admin\Filters\Order\OrderOrderedAtDateAdminFilter;
use Aero\Admin\Reports\Report;
use Aero\Admin\Reports\Traits\HasFilters;
use Aero\Cart\Models\Order;
use Illuminate\Database\Eloquent\Builder;
class OrdersReport extends Report
{
   use HasFilters;
   protected function newQuery(): Builder
   {
       return Order::with([
           'status', 'items', 'currency', 'discounts', 'payments.method',
       ]);
   }
   protected static $filters = [
       OrderOrderedAtDateAdminFilter::class,
   ];
}

Admin Slots
Admin Slots

What are the available backend admin slots?
For more information about using backend admin slots please see our article "How do I inject a view into a backend admin slot?
The following slots can be injected into.
app- 
scripts- 
login.header- 
login.form- 
login.footer- 
login.footer- 
catalog.product.new.cards- 
catalog.product.edit.cards- 
catalog.product.new.header.buttons- 
catalog.product.edit.header.buttons- 
catalog.category.new.cards- 
catalog.category.edit.cards- 
orders.order.view.header.buttons- 
orders.order.view.cards.top- 
orders.order.view.cards.middle- 
orders.order.view.cards.bottom- 
orders.order.view.cards.extra.top- 
orders.order.view.cards.extra.info- 
orders.order.view.cards.extra.middle- 
orders.order.view.cards.extra.bottom- 
customer.new.header.buttons- 
customer.new.cards- 
customer.edit.header.buttons- 
customer.edit.cards- 
customer.edit.extra.bottom- 
customer.edit.extra.sidebar- 
catalog.attribute-groups.index.header.buttons- 
catalog.categories.index.header.buttons- 
catalog.collections.index.header.buttons- 
catalog.manufacturers.index.header.buttons- 
catalog.products.index.header.buttons- 
catalog.tag-groups.index.header.buttons- 
content.blocks.index.header.buttons- 
content.combinations.index.header.buttons- 
content.missing-pages.index.header.buttons- 
content.pages.index.header.buttons- 
content.redirects.index.header.buttons- 
customers.groups.index.header.buttons- 
customers.index.header.buttons- 
discounts.edit.cards- 
discounts.edit.extra.sidebar- 
discounts.edit.header.buttons- 
discounts.index.header.buttons- 
discounts.new.cards- 
discounts.new.extra.sidebar- 
discounts.new.header.buttons- 
discounts.vouchers.index.header.buttons- 
orders.index.header.buttons- 
price-lists.index.header.buttons- 
settings.countries.index.header.buttons- 
settings.currencies.index.header.buttons- 
settings.customer-tax-groups.index.header.buttons- 
settings.fulfillment-methods.index.header.buttons- 
settings.mail.index.header.buttons- 
settings.order-statuses.index.header.buttons- 
settings.payment-methods.index.header.buttons- 
settings.price-groups.index.header.buttons- 
settings.product-tax-groups.index.header.buttons- 
settings.shipping-methods.index.header.buttons- 
settings.subscription-plans.index.header.buttons- 
settings.tax-rates.index.header.buttons- 
settings.tax-rules.index.header.buttons- 
settings.users.index.header.buttons- 
configuration.fulfillment-methods.edit.cards- 
configuration.fulfillment-methods.new.cards- 
configuration.shipping-methods.edit.cards- 
configuration.shipping-methods.new.cards- 
orders.fulfillment.edit.extra.info- 
orders.fulfillment.edit.cards- 
orders.fulfillment.edit.extra.sidebar- 
orders.fulfillment.new.extra.info- 
orders.fulfillment.new.cards- 
orders.fulfillment.new.sidebar- 
subscriptions.subscription.view.extra.form- 
subscriptions.subscription.view.cards- 
subscriptions.subscription.view.extra.info- 
subscriptions.subscription.view.extra.sidebar

How do I inject a view into a backend admin slot?
Any view you wish to inject into a slot should be defined in a Service Provider.
If the code is unique to a particular store it could be placed in the boot method of the AppServiceProvider. If it is to be included as part of a module, then it can be added in the setup method of the module's ServiceProvider.
When registering the view to be injected into the slot, you must provide the slot name along with the view, for example:
<?php
namespace Acme\MyModule;
use Aero\Admin\AdminSlot;
use Aero\Common\Providers\ModuleServiceProvider;
class ServiceProvider extends ModuleServiceProvider
{
    public function setup(): void 
    {
        AdminSlot::inject('catalog.product.edit.cards', 'my-module::admin-slot');
    }
}
For advanced cases where additional data needs to be passed into your view, a closure can be used. The closure receives a $dataparameter that contains the current variables on the page.
AdminSlot::inject('catalog.product.edit.cards', function ($data) {
    // add custom variables to the $data array
    return view('my-module::admin-slot', $data);
});

Admin Transformers
Admin Transformers

What are the available transformers?
The following classes are in the Aero\Admin\Transformersnamespace.
Class
Description
AttributeTransformer
This transformer is used in the ProductTransformer to transform the product's attribute groups.
BaseVariantTransformer
This transformer is used to generate the data for a new variant.
ProductTransformer
This transformer is used to transform a product for the product new/edit page.
VariantTransformer
This transformer is used in the ProductTransformer to transform the product's variants.
DiscountTransformer
This transformer is used to transform a discount for the discount new/edit page.

How do I add custom data to a transformer?
To add custom data you need to use the static **add()**method on the relevant transformer. This method expects a closure that will return an array of the field(s) to merge into the transformers array. The closure will be passed an array of data that you can use.
This code example adds a customFieldthat will have the value of the products ID multiplied by two. This field will then be usable on the product new/edit page directly in Vue like product.customField. It’s important to note that this is used on the new product page where the product will not have an ID and some other data which is why the ?? 0is used.
<?php
namespace Acme\MyModule;
use Aero\Admin\Transformers\ProductTransformer;
use Aero\Common\Providers\ModuleServiceProvider;
class ServiceProvider extends ModuleServiceProvider
{
   public function setup()
   {
       ProductTransformer::add(function ($data) {
           /* @var \Aero\Catalog\Models\Product */
           $product = $data['product'];
           return [
               'customField' => ($product->id ?? 0) * 2,
           ];
       });
   }
}
Casts
When the admin page fails validation and redirects back with the old data, the old data is casted to ensure it’s correct using the transformer. This is particularly important for booleans where a checkbox may set the value as 1 and it therefore should be casted to true to avoid problems.
Available Cast Types
You can currently use the following casts: boolean, array, string, and integer.
Adding a Cast
To add a cast you need to use the static **addCast()**method on the relevant transformer. This method expects two parameters, the key of the field and the value that the field should be casted to.
This code example ensures the customFieldis casted as an integer.
<?php
namespace Acme\MyModule;
use Aero\Admin\Transformers\ProductTransformer;
use Aero\Common\Providers\ModuleServiceProvider;
class ServiceProvider extends ModuleServiceProvider
{
   public function setup()
   {
       ProductTransformer::addCast('customField', 'integer');
       ProductTransformer::add(function ($data) {
           /* @var \Aero\Catalog\Models\Product */
           $product = $data['product'];
           return [
               'customField' => ($product->id ?? 0) * 2,
           ];
       });
   }
}

Admin Vue
Admin Vue

How do I add custom clips to the redactor editor
Clips is a plugin for the Redactor editor that allows you to create a list of frequently used code to be used in the editor.
In the following example we will add a label clip that will allow users to use a label in the Redactor editor that looks like this:
You need to use the stylesand scriptsslots to inject views across the whole admin that include the CSS for the clip (in this case the CSS for the label) and the JS for creating the clip. This can be done in a module service providers setupmethod or the app service providers bootmethod. If you use a module service provider, do not forget to register your views under your modules namespace using the $this->loadViewsFrommethod.
<?php
namespace Acme\MyModule;
use Aero\Admin\AdminSlot;
use Aero\Common\Providers\ModuleServiceProvider;
class ServiceProvider extends ModuleServiceProvider
{
   public function setup()
   {
       $this->loadViewsFrom(__DIR__.'/../resources/views', 'my-module');
       AdminSlot::inject('styles', 'my-module::clips-styles');
       AdminSlot::inject('scripts', 'my-module::clips-scripts');
   }
}
The my-module::clips-stylesview should contain a style tag with the CSS for the label. Any CSS for a clip should be scoped to the .redactor-boxclass in this view. You will also need to put this CSS in your themes CSS file so that it works on the storefront (this CSS doesn’t need to be scoped to the .redactor-boxclass).
<style>
   .redactor-box .label-red {
       display: inline-block;
       background-color: #ec2d80;
       color: #fff;
       line-height: 1;
       padding: 2px 8px;
       border-radius: 4px;
       font-weight: normal;
   }
</style>
The my-module::clips-scriptsview should contain a script tag with the JS for the label. The first parameter is the text shown in the clips list and the second parameter is the code inserted when the clip is used.
<script>
   window.RedactorClips.add('Red label', '<b class="label-red">Label</b>');
</script>
Completing these steps will add a clips button to the top of the Redactor editor that when clicked will list your registered clips that can then be clicked to insert their code.

How do I use Vue in the admin?
When in an admin's Blade view file you can use any Vue directive (such as 
v-for, 
v-if, 
@clicketc).
Putting Vue into Dev Mode
Putting Vue into dev mode allows you to use the Vue dev tools. You can put the Admin Vue into dev mode by putting this code snippet in your Blade view:
@push('scripts')
   <script>
       window.AeroAdmin.vue.use({
           install(Vue) {
               Vue.config.devtools = true;
           },
       });
   </script>
@endpush
Extending Vue without a Custom Component
You can extend Vue without using a custom Vue component by using some of the 
window.AeroAdminmethods. Methods are provided for you to add data, methods, computed methods, watchers, and listeners.
Adding Data
You can make data available in Vue through the 
window.AeroAdmin.addDatamethod. This method expects two parameters, the variable name and the variable value. You can pass JSON as the values for complex data types such as arrays.
@push('scripts')
   <script>
       window.AeroAdmin.addData('myVariable', '{{ old('myVariable', 'value') }}');
       window.AeroAdmin.addData('myOtherVariable', 'Example');
       window.AeroAdmin.addData('myArrayVariable', {!! json_encode(['one', 'two', 'three', 'four']) !!});
   </script>
@endpush
Adding a Method
You can make Vue methods using the 
window.AeroAdmin.addMethodmethod. This method expects two parameters, the method name and a closure defining the method. Inside of the closure you can use thisto reference the Vue instance (this allows you to call other methods or interact with the data variables (anything you can do from within Vue).
@push('scripts')
   <script>
       window.AeroAdmin.addMethod('test', function (value) {
           console.log(value);
       });
       window.AeroAdmin.addMethod('testing', function (value) {
           this.test(value);
       })
   </script>
@endpush
Adding a Watcher
You can make a Vue watcher using the 
window.AeroAdmin.addWatchmethod. This method expects two parameters, the watcher name and a closure defining the watcher. Inside of the closure you can use thisto reference the Vue instance (this allows you to call other methods or interact with the data variables (anything you can do from within Vue).
@push('scripts')
   <script>
       window.AeroAdmin.addData('testing', 'value');
       window.AeroAdmin.addMethod('test', function (value) {
           console.log(value);
       });
       window.AeroAdmin.addWatch('testing', function (value) {
           this.test(value);
       })
   </script>
@endpush
Adding a Computed Method
You can make Vue computed methods using the 
window.AeroAdmin.addComputedmethod. This method expects two parameters, the computed method name and a closure defining the computed method. Inside of the closure you can use thisto reference the Vue instance (this allows you to call other methods or interact with the data variables (anything you can do from within Vue).
@push('scripts')
   <script>
       window.AeroAdmin.addComputed('computedMethod', function () {
           return ['one', 'two', 'three', 'four'];
       });
   </script>
@endpush
Adding a Listener
You can add a listener to Vue using the 
window.AeroAdmin.addListenermethod. This method expects two parameters, the event name to listener for and a closure defining what to do when the event is emitted. Inside of the closure you can use thisto reference the Vue instance (this allows you to call other methods or interact with the data variables (anything you can do from within Vue).
@push('scripts')
   <script>
       window.AeroAdmin.addListener('loaded', function () {
           console.log('Vue has been loaded');
       });
   </script>
@endpush

How do I add a custom Vue component to the admin?
This repo (https://github.com/aerocargo/admin-example-vue
You need to add the 
assetLinksfunction to your modules service provider so that your modules assets are publicly linkable. An example module service provider can be found here in the repo (https://github.com/aerocargo/admin-example-vue/blob/master/src/ServiceProvider.php
You also need to make your component 
.vuefile(s) and your main 
.jsfile that will register all of the components. An example javascript structure can be found here in the repo (https://github.com/aerocargo/admin-example-vue/tree/master/resources/js
Once you’ve got your files setup and have built the javascript files (using npm - 
npm run devor 
npm run production), you need to ensure that your module's components javascript file is loaded and the components are registered in the Blade view where you want to use the component(s). To do this you need to include the javascript file from your module and then register the components through the 
window.AeroAdmin.vue.usemethod.
@push('scripts')
   <script src="{{ asset(mix('components.js', 'modules/aerocargo/admin-example-vue')) }}"></script>
   <script>
       window.AeroAdmin.vue.use(window.exampleAdminComponents);
   </script>
@endpush
Once you have completed this you’ll be able to use your custom Vue component(s) anywhere in the Blade file where you have registered them.

Blocks
Blocks

How do I import content blocks?
In order to export content blocks, we will require an Aero package. To install this package, type the following command in the project root directory:
composer require aerocargo/blocks-import-export
After installing the package, we can navigate to the adminthen Content Managementand finally, Blocks. In the top right corner of this page, we now have a button for importing blocks.
The .zip file required by the importer can only be generated by the content blocks exporter. If we do have an exported .zip file we can simply upload it within the modal that appears on button click. After uploading the .zip file and letting the blocks import, they should appear in the list.

How do I export content blocks?
In order to export content blocks, we require an Aero package. To install this package, type the following command in the project root directory:
composer require aerocargo/blocks-import-export
After the package has been installed successfully, we can navigate to the admin, then to Content Management, and lastly to the Blocks. We can now use the bulk action to select which blocks we wish to export, otherwise if we wish to export all of the content blocks we select the checkbox next to the name header field of the table.
Finally, we apply the “Export blocks” bulk action at which point the export will generate the necessary blocks and download them to the user’s browser.

Introduction to blocks
Whilst the layout of the storefront is dictated by the theme, the displayed content is managed through blocks. This includes navigation (such as header and footer menus), promotional imagery, static text and HTML snippets. Each block can contain multiple items of different types, allowing for features like image carousels and mega menus.

Configuration
Configuration

How do I register new permissions?
// add a single permission
\Aero\Admin\Permissions::add('custom');
// add multiple permissions
\Aero\Admin\Permissions::add(['custom', 'custom.view', 'custom.manage']);

How do I check for permissions?
When checking for permission, you should always be as explicit as you can. For example, checking a user has the 
custom.viewor 
custom.managepermissions.
Guarding content in a Blade file:
@can('custom.view')
    ...
@endcan
@canany(['orders', 'custom'])
    ...
@endcan
Checking the admin user has permission to access an endpoint:
Route::get('/my-module', [MyModuleController::class, 'index'])
    ->name('admin.modules.my-module')
    ->middleware('can:custom.view');
Route::get('/my-module/manage', [MyModuleController::class, 'manage'])
    ->name('admin.modules.my-module.manage')
    ->middleware('can:custom.manage');
Guarding the execution of code in a Controller:
if ($this->can('custom')) {
    // ...
}
if ($this->canAny(['orders', 'custom'])) {
    // ...
}

How do I change how order references are generated?
You can set an order reference generator to define how order references are generated. To do this you need to use the static setReferenceGeneratormethod on the Aero\Cart\Models\Ordermodel.
The method accepts a closure that accepts the order as a parameter and expects you to return a string (the reference for the order passed into the closure).
If you do not set an order reference generator the default generator will be used that does the following:
<?php
namespace Acme\MyModule;
use Aero\Cart\Models\Order;
use Aero\Common\Providers\ModuleServiceProvider;
class ServiceProvider extends ModuleServiceProvider
{
   public function setup()
   {
       Order::setReferenceGenerator(function (Order $order) {
           $prefix = setting('order_reference_prefix');
           $random = substr(str_shuffle(str_repeat('0123456789', 10)), 1, 3);
           return "{$prefix}{$order->getKey()}-{$random}";
       });
   }
}

What is the robots file and how does it differ from normal?
The robots.txt file provides instructions for web crawlers and bots that index your store. Aero automatically registers core parts of the store that should avoid being indexed and visited by bots.
In order to extend this file and add modules or store specific restrictions, the robots.txt file is processed as a pipeline, where the content (the text) is made available and can be manipulated.
For ease of access, the $contentpassed through the pipeline is an Illuminate\Support\Collection, grouped by the user agent.
\Aero\Store\Pipelines\RobotsTxt::extend(static function ($content) {
    if ($agent = $content->get('User-agent: *')) {
        $agent->push('Disallow: /my_module/');
    }
});

How do I store product images on S3
You need to configure and set up your Laravel project for the S3 storage driver. You can read more about this in the laravel documentationAWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_DEFAULT_REGION, and AWS_BUCKET) and installing a composer package (league/flysystem-aws-s3-v3 ~1.0) that lets Laravel support S3.
Once your project is configured you should set STORE_FILE_DRIVERand STORE_LOCAL_FILE_DRIVERto your S3 driver. If you have followed the Laravel documentation steps above and are using the default S3 disk already created you will simply add this to your env file:
STORE_FILE_DRIVER=s3
STORE_LOCAL_FILE_DRIVER=s3
STORE_FILE_DRIVER
This by default is set as public and is the disk used by Aero when uploading files that should be publicly accessible.
STORE_LOCAL_FILE_DRIVER
This by default is set as local and is the disk used by Aero when uploading files that should not be publicly accessible directly.

Customization
Customization

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::$nameContainsManufacturerboolean 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;
   }
}

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::$slugContainsManufacturerboolean 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;
   }
}

Developer Faqs
Developer Faqs

Does Aero support Laravel Homestead installation?
Yes, Homestead is supported on Windows, Mac or Linux provided your Vagrant box configuration meets the system requirements

Can I use Aero project credentials across multiple projects?
No, Credentials must not be shared between projects.

Is Laravel required to run Aero?
Yes, the Aero platform is built on top of the Laravel framework. Knowledge of Laravel is not essential for basic Aero development but will help with more complex actions and configurations.

What versions of Elasticsearch does Aero support?
Aero supports both Elasticsearch 6.x and 7.x.
Support for Elasticsearch 6.x is provided by the 0.x version of 
aerocommerce/elastic-search.
Support for Elasticsearch 7.x is provided by the 1.x version of 
aerocommerce/elastic-search.

Which versions of PHP will Aero run on?
Aero requires PHP Version 7.2 and above.

How do I configure the PHP memory limit?
Due to the number of fields that are sometimes present in the admin, it may be necessary to adjust the PHP 
max_input_varsvalue. The default of 1,000 fields can be too small when managing a product that contains hundereds of variants. This limit is quickly reached when considering how many fields each variant has.
Data will be truncated by the server
A form that contains more fields than PHP is configured to accept will result in lost or corrupted data.
The 
max_input_varsvalue is defined in the php.ini file. On a Linux server, this is typically located:
# PHP 7.3
/etc/php/7.3/php-fpm/php.ini
# PHP 7.4
/etc/php/7.4/php-fpm/php.ini
# PHP 8.0
/etc/php/8.0/php-fpm/php.ini
On macOS when using Homebrew:
# PHP 7.3
/usr/local/etc/php/7.3/php.ini
# PHP 7.4
/usr/local/etc/php/7.4/php.ini
# PHP 8.0
/usr/local/etc/php/8.0/php.ini
We recommend a value of 
10,000. You should either search for the existing entry, or add to the bottom of the file:
max_input_vars=10000
Once set, you'll need to reload PHP-fpm.
# Linux (PHP 7.3)
sudo service php7.3-fpm reload
# Linux (PHP 7.4)
sudo service php7.4-fpm reload
# Linux (PHP 8.0)
sudo service php8.0-fpm reload
# macOS (Laravel Valet)
valet restart

Events
Events

How do I create a new event listener?
To create a new listener class for one of our events, we can use Artisan to scaffold the listener from the project root directory:
php artisan make:listener UpdateDetails
After adding all the necessary functionality for our listener, we have to make Aero aware of it by adding it to a listen property, wrapped within an event like so:
protected $listen = [
    Registered::class => [
            UpdateDetails::class,
    ]
];
There is also an alternative, for events that should not be queued:
​​Event::listen(OrderPlaced::class, function ($event) {
    $order = $event->order;
    //...
});
Extending ManagedListener
In order to use a ManagedListener, we have to have an event extending ManagedEvent. This can be accomplished like so:
use Aero\Events\ManagedHandler;
protected $listen = [
    FormSubmitted::class => [
        ManagedHandler::class,
    ],
];

What are the events available?
Account
AddressCreated - AddressDeleted - AddessUpdated - CustomerManualylCreated - CustomerRegistered - CustomerUpdated - CustomerChanged - PasswordResetRequest
Cart
CartEmptied - CartItemAdded - CartItemRemoved - CartItemUpdated - OrderCanceled - OrderClosed - OrderComplete - OrderConfirmation - OrderDispatched - OrderItemBought - OrderOnHold - OrderPartiallyDispatched - OrderPartiallyReturned - OrderPlaced - OrderProcessing - OrderReturned - OrderStatusUpdated - OrderSuccessful - OrderUpdated
Catalog
AttributesDeleted - AttributesUpdated - CategoryDeleted - CategoryUpdated - ListingCreated - ListingsDeleted - ListingsUpdated - ListingUpdated - ManufacturerUpdated - ProductCreated - ProductDeleted - ProductUpdated - TagsUpdated
Content
BlockCreated - BlockDeleted - BlockUpdated
Fulfillment
FulfillmentDispatched
Payment
PaymentCanceled - PaymentCaptured - PaymentFailed - PaymentRefunded
Redirector
RedirectHit - RedirectNotFound
Store
FormSubmitted
Subscription
SubscriptionCanceled - SubscriptionCardExpired - SubscriptionCreated - SubscriptionFailed - SubscriptionOrderFailed - SubscriptionPaused - SubscriptionPaymentFailed - SubscriptionPaymentSuccess - SubscriptionShippingFailed - SubscriptionSuccessful - SubscriptionSuccessful - SubscriptionUnpaused - SubscriptionUpcoming

How do I add a custom event to a Service Provider?
Laravel events provide an observer implementation, allowing you to listen and observe for specific tasks happening in your application. Adding a custom event to a ServiceProvidermeans that the application will listen to it as soon as it occurs. In terms of choosing the right ServiceProviderfor the job, it breaks down into two options: a Module ServiceProvider, or any of the Providers listed under app/Providersof our shop’s root directory.
Event directory:
app/Events
Listener directory:
app/Listeners
The ServiceProviderclass has a $listenproperty, to which we simply add on the Event that we wish to add, alongside its listener.

What are event listeners?
Event listeners, as the name suggests, listen to events that have been assigned to them. This means that listeners have to first be manually mapped for which events they should listen to.
Mappings for Event Listenersare declared in the appropriate Service Provider, depending on what part of the platform it affects - for example, it could be a Module Service Provideror the EventServiceProviderin the app directory of Aero.
How do I add a listener to an event?
<?php
namespace App\Providers;
use Illuminate\Auth\Events\Registered;
use Illuminate\Auth\Listeners\SendEmailVerificationNotification;
use Illuminate\Foundation\Support\Providers\EventServiceProvider as ServiceProvider;
use Illuminate\Support\Facades\Event;
class EventServiceProvider extends ServiceProvider
{
    /**
     * The event listener mappings for the application.
     *
     * @var array
     */
    protected $listen = [
        Registered::class => [
            SendEmailVerificationNotification::class,
        ],
    ];
    /**
     * Register any events for your application.
     *
     * @return void
     */
    public function boot()
    {
        parent::boot();
        //
    }
}
The listener is always nested within the event which it affects so that it can pass on the event to the listener’s handle method. Remember that the listener always goes within the array instantiating from the event class.

How do I create a new event?
To create a new event, we first have to scaffold the class using Artisan in the project root directory:
php artisan make:event Registered
The above command scaffolds a class into the app/Eventsand we can modify it accordingly to our needs.
After applying all the necessary functionality to the event, we have to also create a listener to listen to an event happening. In order for Aero to recognize the event, we have to specify it in a Service Providerof choice, for example, a module Service Provider:
protected $listen = [
        Registered::class => [
                // insert your listener here
        ]
];
Extending ManagedEvent
Aero’s ManagedEventis an optional classmade specifically for Aero events and is used mainly for mailing events. These are a special type of event as all the events that extend ManagedEventare actions.
It’s important to note that the listener that listens to an event extending ManagedEvent, must also extend the ManagedListenerclass.
Variables
With ManagedEvent, we can create variables that can then be accessible in a mailing template. In essence, variables create helper text and allow the user to use data from the event itself. To define some variables, we have to create a property ‘variables’ in our event:
Adding variables
public static $variables = [
    'product.slug',
    'product.name',
    'product.model',
    'product.summary',
    'product.description',
    'product.thumbnail',
    'product.heading',
    'product.url',
    'product.categories.*.name',
    'product.categories.*.has_featured_image',
    'product.categories.*.featured_image_file',
    'product.manufacturer.name',
    'product.manufacturer.has_logo',
    'product.manufacturer.logo_file',
    'product.attributes.*.name',
    'product.attributes.*.image_file',
    'product.images.*.image_file',
    'product.all_images.*.image_file',
    'product.variants.*.sku',
    'product.variants.*.has_stock',
    'product.highest_price',
    'product.lowest_price',
    'product.has_reductions',
];
The above variables will then be accessible in our mail template.
An alternative method of adding variables:
EventClass::addVariable(‘product.lowest_price’)
Or multiple, as an array:
EventClass::addVariables([‘product.manufacturer.name’, ‘product.lowest_price’])

How do I make custom mail notification events?
To create an event that shows up in the Mail Notifications part of the admin your event needs to extend Aero\Events\ManagedEventand have Aero\Events\ManagedHandleras a registered listener.
To create an event you need to create a class that extends Aero\Events\ManagedEvent.
<?php
namespace Acme\MyModule\Events;
use Aero\Events\ManagedEvent;
class MyEvent extends ManagedEvent
{
   //
}
To register Aero\Events\ManagedHandleras a listener of your event you need to add your event and the managed handler to a $listenarray inside of a service provider.
<?php
namespace Acme\MyModule;
use Acme\MyModule\Events\MyEvent;
use Aero\Common\Providers\ModuleServiceProvider;
use Aero\Events\ManagedHandler;
class ServiceProvider extends ModuleServiceProvider
{
   protected $listen = [
       MyEvent::class => [
           ManagedHandler::class,
       ],
   ];
   public function setup()
   {
       //
   }
}

Extending Core Functionality
Extending Core Functionality

What are models and how do I use them?
All models provided as part of the Aero core platform are Eloquent models
For example, if a module provided 
reviewsfor products, the 
reviewsmethod 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 
Collectionof reviews can be accessed through the magic property:
$reviews = $product->reviews;
{% for review in product.reviews %}
    ...
{% endfor %}

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->loadViewsFrommethod and inject them into the relevant slots using the 
Aero\Admin\AdminSlot::injectmethod.
<?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
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\CreateProductRequestand 
Aero\Admin\Http\Requests\Catalog\UpdateProductRequestvalidators. To do this we need to use the 
expectsmethod 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\VariantTransformertransformers.
<?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' => '',
           ];
       });
   }
}

What are the available commands?
php artisan make:module
This command scaffolds a module for you (you need to provide a valid name for the module such as aerocargo/my-module). This involves setting up composer and publishing some default module stubs.
php artisan aero:configure
This command walks you through configuring your store. This includes things such as setting the store's name, database connection, and Elasticsearch connection.
php artisan aero:install
This command runs you through the installation process for your store. This includes completing migrations, configuring Elasticsearch, linking storage, seeding data, and clearing the applications cache.
php artisan aero:link
This command calls the patch command, links theme/module assets, clears the Twig views cache, and clears the Laravel views cache.
php artisan module:install
This command will install a specific module after you have composer required the module. This command seeds any data for the module, optionally clears the application cache, and links the module assets. You should note that this command doesn’t have to be used and is automatically run when composer requiring a module.
php artisan aero:patch
This command patches your store's database. This command is run automatically when updating and ensures that your database is kept up to date with Aeros latest updates.
php artisan aero:search:install
This command sets up the necessary environment for your search driver. By default when using Elasticsearch this command will create the required indexes.
php artisan aero:search:rebuild
This command resets up and reindexes your search by running the search install and reindex commands.
php artisan aero:search:reindex
This command will reindex your database data into your searches index. By default when using Elasticsearch this command will clear the index and then index your database data.
php artisan aero:seed:emails
This command will seed some default email templates to your mail notifications.
php artisan aero:sitemap:cms
This command will generate the pages-sitemap.xml sitemap file in your public storage directory. This sitemap covers any pages you create through the admin.
php artisan aero:sitemap:combinations
This command will generate the combinations-sitemap.xml file in your public storage directory. This sitemap covers any combinations you create through the admin.
php artisan aero:sitemap:generate
This command will generate a complete sitemap for your store. This generates a sitemap.xml file in your public storage directory that references individual sitemap files for products, combinations, and pages.
php artisan aero:sitemap:products
This command will generate the products-sitemap.xml sitemap file in your public storage directory. This sitemap covers any products you create through the admin.
php artisan aero:subscriptions:alert-upcoming
This command will look for upcoming subscriptions and emit the **Aero\Subscription\Events\SubscriptionUpcoming **event for them.
php artisan aero:subscriptions:check-cards
This command will look for expired cards on subscriptions and then cancel the subscription and emit the Aero\Subscription\Events\SubscriptionCardExpiredevent for them.
php artisan aero:subscriptions:process
This command will process any subscriptions that need processing.

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_3field to the 
addresses, 
order_addresses, and 
fulfillment_addressestables.
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.inputview 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\AddressFormto add the field to the frontend address forms (checkout and account-area) and 
Aero\Admin\Http\Forms\AdminAddressFormto 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\AddressFormatterand 
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');
       });
   }
}

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_sectionssection (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 
sectionssection.
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\AddressFormclass.
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 extendsstatic 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 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\CanHaveSettingstrait 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\CanHaveSettingstrait 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-settingsview 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 
rulesmethod to merge in the settings rules with your current rules. To do this you need to 
array_mergeyour rules with the array of rules returned by the 
Aero\Admin\Utils\SettingHelpers::getRulesForModelmethod. 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 
prepareForValidationmethod 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::saveForModelmethod, 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 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 
$pricesarray 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 
$pricesarray 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->loadViewsFrommethod in the module service providers 
setupmethod.
Next we’re going to use the 
Aero\Store\Http\Responses\ProductPageresponse 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\ContentForBodypipeline 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.twigfile 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_pricesvariable 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 }}">&nbsp;{{ 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.loadedAero Event. This code will loop over all input checkbox elements that have the 
data-extra-price-incattribute and add an input event listener. We’ll add a 
updateAndSetAdditionalPricesfunction 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-incattribute 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-cartAero Event. This code loops over all input checked checkboxes that have the 
data-extra-price-incattribute 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\CartItemBuilderand 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\CartItemOptionto the 
Aero\Cart\CartItemwith 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);
           });
       });
   }
}

General
General

How do I set up a slug validator?
You can control if a collection of slugs is valid or not by setting your own validator:
\Aero\Store\Routing\Slugs::setValidator(function ($slugs, $resolver) {
    if ($resolver !== 'listings') return true;
    if ($slugs->count() < 3 && $slugs->where('model_type', 'tag')->count()) return false;
    return true;
});

How do I add a route to the CSRF exception list?
If you do not want your route to require a valid CSRF token
Code
To add a route to the exceptions list through code you need to add to the static VerifyCsrfToken::$exceptionsarray.
<?php
namespace Acme\MyModule;
use Aero\Common\Providers\ModuleServiceProvider;
use Aero\Store\Http\Middleware\VerifyCsrfToken;
class ServiceProvider extends ModuleServiceProvider
{
   public function setup()
   {
       VerifyCsrfToken::$exceptions[] = '/my-route';
   }
}
Configuration
The routingconfiguration file has a csrf_exceptionsarray where you can list routes for the exceptions list.
You can publish the routingconfiguration file with this command:
php artisan vendor:publish --provider="Aero\Routing\Providers\RoutingServiceProvider"
Once this file is published you can view it by navigating to config/aero/routing.phpin your project. You can edit the csrf_exceptionsarray directly in this file.
You may need to clear the config cache for your changes to take effect.
php artisan config:clear

What queues does Aero use?
Queues are used so that HTTP requests can remain snappy and more intensive code can be executed later. You can learn more about queues here
Queue
Description
search
This queue has search jobs. The majority of these jobs will be responsible for reindexing listings.
email
This queue has email jobs. Whenever an email is sent and queued in your application, it will be in this queue.
subscriptions
This queue has subscription jobs. The majority of these jobs are responsible for creating and processing subscriptions.
default
This queue has any jobs that are not in one of the other queues. These jobs can be anything from order created event handlers to customer created event handlers.
Aero uses four queues so that you can have different workers completing different tasks without a risk of locks or jobs being handled more than once. It’s important to note that some queues should be considered more important than others (for example, processing events such as an order created event is more important than updating your search index). In a production environment you should use tools like Supervisor to manage your queue workers.
You can use the queue work command to run all of or only specific queues (if you adjust the list of queues in the command). It’s important to note that the order of the queues in the command defines their priority.

How to Set the Page Title for the Admin
By default the page title for every admin page is generated from the route name. If you would like to manually set the page title you can set a title specifically for your route or you can return an 
adminPageTitlevariable to your view (this includes extending a response builder and adding this to the data).
To set the title manually for a specific route you can use the 
Aero\Admin\Http\Middleware\SetTitle::addTitle()method. This method expects a route name and a page title.
<?php
namespace Acme\MyModule;
use Aero\Admin\Http\Middleware\SetTitle;
use Aero\Common\Providers\ModuleServiceProvider;
class ServiceProvider extends ModuleServiceProvider
{
   public function setup()
   {
       SetTitle::addTitle('admin.dashboard', 'The Dashboard Title');
   }
}

Google Shopping Feed
Google Shopping Feed

How do I configure the Google Shopping feed?
Generating the taxonomy
Generating the taxonomy table is a required task after we’ve initially installed the package, and is the source of all the categories provided by google shopping.
To generate the taxonomies table we can:
Navigate to the modules’ section in the admin and generate taxonomies through the modules’ interface 2. Run a command from the root project directory:
php artisan aero:google:taxonomies
Setting a CRON run at time
We can specify the time at which the taxonomies should be updated automatically. This can be changed by:
Editing the config.php file and manually adding the CRON time 2. Setting the value in the admin modules’ section under Configuration in the Google Shopping module.
Query parameters
Query parameters are used for every variant link that is input into the XML feed. These can be used to grab different prices such as included and excluded VAT. To set query parameters:
Navigate to the modules section of the admin, go into Google Shopping and Configure. 2. Add your query parameters without any double or single quotation symbols in the following format:
KEY=VALUE for a single parameter
KEY=VALUE, KEY2=VALUE2 and so on for multiple parameters
Click on save
Each of our variants should now be output into the XML feed with the query parameters we have specified.

How do I assign Google categories?
Google categories can be assigned to categories, or directly to the product. It can be set up either way as the mapping tool automatically detects the ones that have been set up. There is also a way of mapping whether a google category is grabbed from categories or the products. It can be done the following way:
To categories
Navigate to categories under Catalog Management. 2. Choose the category we want to assign a Google Category to from the bottom of the page. 3. Save the category.
To products
Navigate to products under Catalog Management. 2. Choose the category we want to assign a Google Category to from the bottom of the page. 3. Save the product.

How do I export the Google Shopping feed?
To export the feed we need to navigate into the module’s section in the admin, and then to Google Shopping. Provided we’ve generated the taxonomies and have configured the package correctly, we can click on the Exportbutton to begin mapping out the feed.
Mapping allows the feed to use alternative fields as a data source for a certain mapping as well as setting a fallback, in case the data source is empty.

How do I generate a Google Shopping feed?
In order to generate a Google Shopping feed, it is necessary to install an Aero dependency in form of the aerocargo/google-shoppingpackage from agora.aerocommerce.com
More developer-specific information about the Google Shopping feed can be found on the Google Shopping module page in agora.
Installing the package
To install the package, simply run the following command from the project’s root directory:
composer require aerocargo/google-shopping
After the package has been installed:
php artisan migrate
And finally, if we wish to modify any of the configuration:
php artisan vendor:publish --provider="Aerocargo\GoogleShopping\ServiceProvider"

Installation Deployment
Installation Deployment

How do I install Aero on Mac?
Before continuing, please ensure your Mac is running macOS Mojave (10.14) or newer.
There are a couple of ways to develop locally on the Mac:
Valet – manually install the services directly on your Mac. - Homestead – a virtual machine that runs the required services.
Valet
Laravel Valet is a development environment for Mac minimalists. It provides a blazing fast local development environment with minimal resource consumption, and points all requests on the 
*.testdomain to sites installed on the local machine.
Step 1: Install Homebrew
Visit the Homebrew website and follow the instructions to install Homebrew.
Make sure everything is up-to-date by running:
brew update
Step 2: Install the services
Run the following command to install the services needed:
brew install php@7.4 mysql openjdk@8 elasticsearch@6 node composer
The services can then be started with the following commands.
Start MySQL:
brew services start mysql
Start Elasticsearch:
brew services start elasticsearch@6
Make sure to place Composer's system‑wide vendor bin directory in your 
$PATH:
export PATH="$PATH:$HOME/.composer/vendor/bin"
Step 3: Install Valet
You can then pull in the Valet package using Composer and install it using the following commands.
First require the Valet package:
composer global require laravel/valet
Then install the package:
valet install
Step 4: Create a database
To connect to the local MySQL server, run the following in your terminal application:
mysql -u root
Once connected, create a new database for your project. It should be named something that relates to your project to ease managing multiple local projects in the future. Run the following SQL, replacing 
[project-name]with the name of your project:
create database [project-name];
If your database name contains hyphens, you must wrap the name in back-ticks, e.g. 
my-first-store.
You can then disconnect from the MySQL server by typing:
exit;
Step 5: Install the Aero Commerce CLI tool
If this is your first project, you'll need to download the command‑line tool using Composer:
composer global require aerocommerce/cli
Once installed, you'll have access to the 
aero newcommand. This will create a fresh project installation in the directory you specify.
For instance, 
aero new myfirststorewill create a directory named 
myfirststorecontaining an Aero project with all dependencies installed:
aero new [project-name]
**During installation, you'll need to enter the package repository credentials and database connection details for the project.**The installer will setup the database tables and seed the project with essential information.
Step 6: Link the project to Valet
The final thing to do is configure the project with Valet. This is achieved by running the 
valet linkcommand from within the project's directory. You can provide a name to use as the domain, for example, to proxy all requests made to 
http://myfirststore.testto the project, the command would be 
valet link myfirststore:
valet link [project-name]
You can now open your web browser and visit your newly created ecommerce store!
Homestead
We are currently unable to offer support for the configuration of Homestead, or any issues associated during the installation process.
Laravel Homestead is a pre-packaged Vagrant box that provides a development environment without requiring you to install any server software on your local machine.
Step 1: Install VirtualBox
Firstly, you'll need to download VirtualBox 6.x. Select the OS X hosts binary and run the installer.
During installation, you may encounter the following error screen:
This is due to security restrictions in macOS. To resolve this, open your terminal application and run the command:
sudo spctl --master-disable
Retry the installation. If the problem still persists, try restarting your Mac and running the installer again.
Once you've successfully completed the installation wizard, you should re-enable Gatekeeper by running the command:
sudo spctl --master-enable
Step 2: Install Vagrant
Head over to the Vagrant download page and select the macOS 64-bit link. Follow the prompts on the installation wizard.
Once complete, you can confirm Vagrant is on your machine by typing the following command into your terminal application to display the Vagrant version:
vagrant -v
Step 3: Install the Homestead Vagrant box
Run the following command in your terminal application:
vagrant box add laravel/homestead
The download may take several minutes, depending on your internet connection.
Step 4: Install Homestead
The Homestead GitHub repository needs to be cloned to your local machine. To do this, run the command:
git clone https://github.com/laravel/homestead.git ~/Homestead
This will create a copy of the repository in a new folder named 
Homesteadwithin your home directory.
Step 5: Configure Homestead
Next, you'll need to create a default 
Homestead.yamlfile which will serve as the instructions to configure your virtual machine. This can be done using the following commands:
$ cd ~/Homestead
$ git checkout release
$ bash init.sh
If you open the newly created Homestead.yaml file, you should see a format similar to this:
---
ip: "192.168.10.10"
memory: 2048
cpus: 2
provider: virtualbox
authorize: ~/.ssh/id_rsa.pub
keys:
    - ~/.ssh/id_rsa
folders:
    - map: ~/myfirststore
      to: /home/vagrant/myfirststore
sites:
    - map: myfirststore.test
      to: /home/vagrant/myfirststore/public
      php: "7.4"
databases:
    - homestead
features:
    - elasticsearch:
        version: "6.8.6"
The 
authorizeand 
keysproperties specify the SSH key pair that will be used to connect to the virtual machine. You can generate the 
id_rsafiles using the command below. There is no need to enter any information when prompted, so just hit enter to use the default values.
ssh-keygen
The 
foldersproperty lists all of the folders to be shared between your Mac and the virtual machine. This allows you to develop using your local IDE (code editor), whilst also providing the web server and other services on the virtual machine access to the project files.
In the example 
Homestead.yamlshown above, we've specified to share the 
~/myfirststorefolder. This is going to be the local directory of our new Aero project, however the folder does not yet exist. To create this directory, run the following command, replacing 
[project-name]with the name of your project:
mkdir ~/[project-name]
This empty directory will be populated later on, from within the virtual machine. The 
tovalue of this folder mapping is the project's path on the virtual machine, e.g. 
/home/vagrant/[project-name].
The 
sitesproperty allows you to define which domains will provide access to the projects. Using the example above, you can see the 
myfirststore.testdomain is configured to point to the project's 
publicdirectory, which is the web server entry point to the project. Ensure the 
tovalue is the virtual machine's project path and not the local one, i.e. 
/home/vagrant/[project-name]/public. Remember to replace 
[project-name]with the name of your project.
Homestead will automatically create the databases listed in the 
databaseproperty of the configuration file. By default, the database name is 
homestead. The installer will automatically detect this database later on, however, if you plan on developing multiple projects, you should name the databases in this list to match your projects.
Finally, you need to ensure that your 
Homestead.yamlconfiguration file contains 
elasticsearchin the 
featuresproperty, along with the 
version: 6declaration.
Step 6: Add the domain to the hosts file
As the project is hosted locally, we need to add an entry to the 
/etc/hostsfile, which will route all requests made to the development domain to your project:
sudo vi /etc/hosts
Open the 
/etc/hostsfile in 
viand add a record to point the domain to the IP address of the virtual machine (the IP listed in the 
Hometead.yamlconfiguration file). Press i to enter insert mode, and use the arrow keys on the keyboard to move the cursor to the bottom of the file. Add the following line, replacing 
[project-name]with the domain name entered in the sites property of your 
Homestead.yamlconfiguration file:
192.168.10.10   [project-name].test
To save the changes and exit the file, press Control + c to leave insert mode, and then type :wq! followed by pressing the Enter key.
Step 7: Provision the virtual machine
You are now ready to boot up the virtual machine. To do so, open your terminal application and navigate to the directory that Homestead was installed in earlier. You can then run the 
vagrant upcommand, which will read from the 
Homestead.yamlfile and configure the virtual machine.
$ cd ~/Homestead
$ vagrant up
Step 8: Install the Aero Commerce CLI tool
The CLI tool needs to be installed on the virtual machine. To do this, SSH into the virtual machine using the command:
vagrant ssh
From within the virtual machine you will need to set the default PHP version by running:
php74
Next, download the command‑line tool using Composer:
composer global require aerocommerce/cli
Once installed, you'll have access to the 
aero newcommand, which needs to be ran from within the project directory on the virtual machine. For instance, running 
aero new .from within the directory 
myfirststorewill scaffold an Aero project with all dependencies installed:
$ cd ~/myfirststore
$ aero new .
**During installation, you'll need to enter the package repository credentials and database connection details for the project.**The installer will setup the database tables and seed the project with essential information.
You can now open your web browser and visit your newly created ecommerce store!

How do I install Aero on Windows?
Homestead
We are currently unable to offer support for the configuration of Homestead, or any issues associated during the installation process.
Laravel Homestead is a pre-packaged Vagrant box that provides a development environment without requiring you to install any server software on your local machine.
Step 1: Enable hardware virtualization
Modern CPUs include hardware virtualization features that help accelerate virtual machines created in VirtualBox. We recommend consulting your computer's user guide on how to enable VT-x/AMD-V in your BIOS or UEFI firmware.
Step 2: Install VirtualBox and Vagrant
Firstly, you'll need to download VirtualBox 6.x. Select the Windows hosts binary and run the installer.
Next we'll head over to the Vagrant download page and select the Windows link. Follow the prompts on the installation wizard.
Once complete, you can confirm Vagrant is on your machine by typing the following command into a PowerShell window:
vagrant -v
Step 3: Install the Homestead Vagrant box
Open up a PowerShell window and run the following command:
vagrant box add laravel/homestead
The download may take several minutes, depending on your internet connection.
Step 4: Install Homestead
The Homestead GitHub repository needs to be cloned to your local machine.
If you have not yet installed Git on your machine, head over to gitforwindows.org to download.
With Git available on your machine, run the following command from within a PowerShell window:
git clone https://github.com/laravel/homestead.git ~/Homestead
This will create a copy of the repository in a new folder named 
Homesteadwithin your home directory.
Next, you'll need to create a default 
Homestead.yamlfile which will serve as the instructions to configure your virtual machine. This can be done using the following commands:
cd ~/Homestead
git checkout release
./init.bat
If you open the newly created Homestead.yaml file, you should see a format similar to this:
---
ip: "192.168.10.10"
memory: 2048
cpus: 2
provider: virtualbox
authorize: C:\Users\user\.ssh\id_rsa.pub
keys:
    - C:\Users\user\.ssh\id_rsa
folders:
    - map: C:\Users\user\Code\myfirststore
      to: /home/vagrant/myfirststore
      type: "nfs"
sites:
    - map: myfirststore.test
      to: /home/vagrant/myfirststore/public
      php: "7.4"
databases:
    - homestead
features:
    - elasticsearch:
        version: "6.8.6"
The 
authorizeand 
keysproperties specify the SSH key pair that will be used to connect to the virtual machine. You can generate the 
id_rsafiles using the command below in a PowerShell window. There is no need to enter any information when prompted, so just hit enter to use the default values.
ssh-keygen
The 
foldersproperty lists all of the folders to be shared between your Windows host and the virtual machine. This allows you to develop using your local IDE (code editor), whilst also providing the web server and other services on the virtual machine access to the project files.
The 
~/syntax is not valid when running on Windows. Instead use the full path, e.g. 
C:\Users\user\myfirststore
In the example 
Homestead.yamlshown above, we've specified to share the 
C:\Users\user\Code\myfirststorefolder. This is going to be the local directory of our new Aero project, however the folder does not yet exist. Create a directory using Windows Explorerand update the 
Homestead.yamlfile.
This empty directory will be populated later on, from within the virtual machine. The 
tovalue of this folder mapping is the project's path on the virtual machine, e.g. 
/home/vagrant/[project-name].
The 
sitesproperty allows you to define which domains will provide access to the projects. Using the example above, you can see the 
myfirststore.testdomain is configured to point to the project's 
publicdirectory, which is the web server entry point to the project. Ensure the 
tovalue is the virtual machine's project path and not the local one, i.e. 
/home/vagrant/[project-name]/public.Remember to replace 
[project-name]with the name of your project.
Homestead will automatically create the databases listed in the 
databaseproperty of the configuration file. By default, the database name is 
homestead. The installer will automatically detect this database later on, however, if you plan on developing multiple projects, you should name the databases in this list to match your projects.
Finally, you need to ensure that your 
Homestead.yamlconfiguration file contains 
elasticsearchin the 
featuresproperty, along with the 
version: 6declaration.
Step 5: Add the domain to the hosts file
As the project is hosted locally, we need to add an entry to the 
C:\Windows\System32\drivers\etc\hostsfile, which will route all requests made to the development domain to your project.
Press the Windowskey and type notepadin the search field. Right-click on Notepadin the search results and select Run as administrator. Open the 
C:\Windows\System32\Drivers\etc\hostsfile and add the following line, replacing 
[project-name]with the domain name entered in the sites property of your 
Homestead.yamlconfiguration file:
192.168.10.10   [project-name].test
Save the file and close Notepad.
Step 6: Add the NFS for Vagrant plugin
A plugin is required when using NFS to sync folders on Windows and will maintain the correct user / group permissions. To install the plugin, run the command in a PowerShell window:
vagrant plugin install vagrant-winnfsd
You'll then need to open up the 
Vagrantfilefound in your Homestead directory and add the following line to the end of the file (within the config area):
config.vm.network "private_network", type: "dhcp"
Step 7: Provision the virtual machine
You are now ready to boot up the virtual machine. To do so, open a PowerShell window and navigate to the directory that Homestead was installed in earlier. You can then run the 
vagrant upcommand, which will read from the 
Homestead.yamlfile and configure the virtual machine. The virtual machine must be ran with administrator privileges to ensure files are synced correctly.
cd ~/Homestead
Start-Process powershell -Verb runAs
vagrant up
Step 8: Install the Aero Commerce CLI tool
The CLI tool needs to be installed on the virtual machine. To do this, SSH into the virtual machine using the following command from a PowerShell window:
vagrant ssh
From within the virtual machine you will need to set the default PHP version by running:
php74
Next, download the command‑line tool using Composer:
composer global require aerocommerce/cli
Once installed, you'll have access to the 
aero newcommand, which needs to be ran from within the project directory on the virtual machine. For instance, running 
aero new .from within the directory 
myfirststorewill scaffold an Aero project with all dependencies installed:
cd ~/myfirststore
aero new .
**During installation, you'll need to enter the package repository credentials and database connection details for the project.**The installer will setup the database tables and seed the project with essential information.
You can now open your web browser and visit your newly created ecommerce store!

What are the system requirements?
Your system must satisfy the following requirements:
PHP (>= 7.2) with BCMath, Ctype, JSON, Mbstring, OpenSSL, PDO, Tokenizer, XML, cURL,GD/ImageMagick
Composer
Nginx or Apache
Node.js (>= 8.10)
MySQL (>= 5.7)
Elasticsearch (6.*)

Introduction
Introduction

Introduction to backend admin slots
Slots allow for a module to include its own view on existing admin pages.
See our section on backend admin slots for more information.

Introduction to collections for developers
Collections are not what the developer might associate with Laravel’s immutable structure, but a collection of rules being applied to the listing on the store’s frontend.
Aero has a variety of collection rules which can alter the filters, more specifically, the search filters that usually would be modified by the customer where products are listed. These include:
Collection rules
Which tags to filter by - Which manufacturer to filter by - Which price range to filter by (from-to) - Which price status to filter by (e.g. Reduced price items only) - Which DateTime range to filter by (range from-to or by days)
For more information about collections please see our admin documentation for collections

Introduction to permissions for developers
As an agency, you will likely need to restrict the information and actions of admin accounts belonging to the retailer.
This is easily set up using our user configuration page. For more information please see "How do I set permissions for users?"
As a developer you can expand on these permissions in order to add anything else you may need;
How do I register new permissions? - How do I check for permissions?

Introduction to transformers
Transformers are used by the admin on some edit pages to transform data from PHP into an json encoded array that can be passed to Vue.
See our section on admin transformers for more information.

Introduction to events for developers
Events are a method of notifying different parts of Aero of tasks that have either been completed or need completing right after something has occurred. Events are only fired when a specified listener has been assigned to it and appropriately notified.
The events are always declared first in any Service Provider, while the listeners are wrapped within the event class. This is because we might need a number of listeners for a given event, each responsible for carrying out different tasks.
See our section on events for more information.
Example of an event:
<?php
namespace Aero\Account\Events;
use Aero\Account\Models\Customer;
use Aero\Events\ManagedEvent;
class CustomerUpdated extends ManagedEvent
{
    public $customer;
    public $customerName;
    public $customerEmail;
    public static $variables = [
        'customer',
        'customerName',
        'customerEmail',
    ];
    public function __construct(Customer $customer)
    {
        $this->customer = $customer;
        $this->customerName = $customer->name;
        $this->customerEmail = $customer->email;
    }
    public function getNotifiable()
    {
        return $this->customer;
    }
}

Introduction to pipelines
Pipelines provide a convenient way to manipulate a given payload. This payload can be an object or a simple string.
See our section on pipelines for more information

Introduction to the account area
The account area provides an extendable area where your customers can view their orders and manage their accounts.
See our section on the account area for more information.

Introduction to response builders
Response builders are Aero's unique solution to serving storefront pages. Based on the concept of pipelines used in Laravel middleware
See our section on response builders for more information.

Introduction to listings and search
Aero offers out-of-the-box listings and search system which is powered by Elasticsearch
Please note the Elasticsearch version is 6.8.
See our section on listings and search for more information.

Introduction to payment drivers
A payment driver is the brain behind the payment method and serves as a bridge to the payment gateway. Whilst there are several integrations that are maintained by the Aero team, a store may require a bespoke integration in order to meet the requirements of a retailer.
See our section on payment drivers for more information.

What is an Aero Commerce Agora account and why do I need it?
To get started you'll need an Agora account.
Your Agora account is used to download the platform and create projects. You can register for an account here
Projects are assigned unique credentials which allows them to purchase themes and modules. These credentials must not be shared between projects as doing so will invalidate them.
Without valid credentials, a project cannot receive platform updates and security fixes.

How do I obtain package repository credentials?
Once logged in to Agora, head over to the projects section and click the New project button. You'll need to give the project a name, for example, the website name or your client's company. In the example below, we've used a fictional company of "Acme Inc".
A domain is optional at this point; you can enter this later when the project is ready to go live. Click the "create new project" button to create the project.
Your new project now appears in the list of projects along with the package repository credentials.

Introduction to the admin
The backend admin (the "admin") is the password-protected back office where administrative tasks can be performed. This includes managing the store's catalog (products, categories, manufacturers, etc.), viewing and processing orders, managing discounts, viewing reports, editing the content of the storefront (promotional imagery, menus and SEO content, etc.), and much more.

Introduction to resource lists
Resource lists allow a table to be extended through additional columns, filters, or sort bys and for bulk actions to be applied.
Please see our section on resource lists for more information.

Introduction to Laravel PHP Framework
The Aero platform is built using the Laravel PHP framework
Before developing with Aero, we recommend that you become acquainted with the core concepts of Laravel. We recommend the following resources:
The official Laravel documentation is a great place to start.
Laracasts offer an on‑demand library of screencast video tutorials.

Listings And Search
Listings And Search

How do I create a custom sort order for the listings pages?
When you set the sort get parameter in a listing pages url (e.g. ?sort=name-az), a method is called on the \Aero\Search\Elastic\Query\Builderclass (for name-az the method sortByNameAzis called). You can find the default sort by methods in the Aero\Search\Elastic\Concerns\CanBeSortedtrait.
To add a custom sort to the listings page you therefore just need to macro a method to the \Aero\Search\Elastic\Query\Builderclass using the correct naming convention.
This example adds an ascending and descending sort by for the summary (to use it you need to add ?sort=summary-az or ?sort=summary-za to the listing page url):
<?php
namespace Acme\MyModule;
use Aero\Common\Providers\ModuleServiceProvider;
class ServiceProvider extends ModuleServiceProvider
{
   public function setup()
   {
       \Aero\Search\Elastic\Query\Builder::macro('sortBySummaryAz', function () {
           $this->query->addSort([
               'string-sort.summary' => [
                   'order' => 'asc',
               ],
           ]);
       });
       \Aero\Search\Elastic\Query\Builder::macro('sortBySummaryZa', function () {
           $this->query->addSort([
               'string-sort.summary' => [
                   'order' => 'desc',
               ],
           ]);
       });
   }
}
If you try the above code you will notice that it throws an Elastica\Exception\ResponseExceptionerror. This is because the string-sort.summaryfield used in the Elasticsearch sort by query does not exist.
To add summaryas a field to the string-sortkey you need to use the **\Aero\Search\Elastic\Documents\ListingDocument::add()**method to define a closure that will return an array of data to merge into the document structure. It’s important to know that when you change the document structure you need to reindex. You can reindex using the php artisan aero:search:reindexcommand.
<?php
namespace Acme\MyModule;
use Aero\Common\Providers\ModuleServiceProvider;
class ServiceProvider extends ModuleServiceProvider
{
   public function setup()
   {
       \Aero\Search\Elastic\Documents\ListingDocument::add('string-sort', function ($document) {
           return [
               'summary' => $document->getModel()->product->getTranslation('summary', request()->store()->language),
           ];
       });
       \Aero\Search\Elastic\Query\Builder::macro('sortBySummaryAz', function () {
           $this->query->addSort([
               'string-sort.summary' => [
                   'order' => 'asc',
               ],
           ]);
       });
       \Aero\Search\Elastic\Query\Builder::macro('sortBySummaryZa', function () {
           $this->query->addSort([
               'string-sort.summary' => [
                   'order' => 'desc',
               ],
           ]);
       });
   }
}
Advanced Query
It’s possible to do a more advanced query for your Elasticsearch sort by. You can make use of Elasticsearch’s Painless scripting language
This example code shows how to add a rand sort that will execute a Java script to sort the listings.
<?php
namespace Acme\MyModule;
use Aero\Common\Providers\ModuleServiceProvider;
use Elastica\Script\Script;
class ServiceProvider extends ModuleServiceProvider
{
   public function setup()
   {
       \Aero\Search\Elastic\Query\Builder::macro('sortByRand', function () {
           $this->query->addSort([
               '_script' => [
                   'type' => 'number',
                   'script' => [
                       'lang' => Script::LANG_PAINLESS,
                       'source' => $this->loadJavaFile(__DIR__.'/../scripts/random-sort.java'),
                       'params' => $this->parameters,
                   ],
                   'order' => 'asc',
               ],
           ]);
       });
   }
}
This Java file is located in a scripts folder outside of the src folder:
def sorts = new ArrayList();
Random rand = new Random();
sorts.add(rand.nextInt(1000000));
if (sorts.size() === 0) {
  sorts.add(0);
}
Collections.min(sorts.stream().map(Double::doubleToRawLongBits).collect(Collectors.toList()));

What are the available documents for listings and search?
There are 2 documents available:
Aero\Search\Elastic\Documents\ListingsDocument
This document is used to hold information about the listings that are shown on the storefront.
Aero\Search\Elastic\Documents\ProductDocument
This document is used to hold information about the products within the store’s catalog. It is generally used in the backend admin, to allow for quick access when managing products.

How do I re-index listing and search documents?
Aero automatically manages indexing changes to documents. For example, if a product's stock level changes the document is updated to reflect the current version. Adding and removing documents is also taken care of for you, sparing the need to schedule in daily tasks to update the store's entire catalog. However, in cases where a large amount of data has been manually changed, or an addition to the document structure has been made, a re-index of all documents can be carried out using the following command:
php artisan aero:search:reindex
If there have only been modifications to a certain document (for example, only the listing document structure has been extended) then the document typecan be passed as an option, which will only re-index the documents with the provided type. The available types correspond to the names of the elastic documents, listing, and product.
php artisan aero:search:reindex --type=listing

How do I add custom facets to listing and search?
Faceted navigation in eCommerce allows customers the ability to refine down a catalog of products based on their filter criteria.
In order to define facets against a document, you must first register the facet group. This should be done within the **boot()**method of a ServiceProvideror the **setup()**method of a module ServiceProvider.
\Aero\Search\FacetSettings::registerFacet('stock', 'Stock Status');
The document structure can then be extended to add the facet to the document’s data.
\Aero\Search\Elastic\Documents\ListingDocument::add('string-facets', function ($document) {
    $stock = $document->getModel()->availableStock() > 0;
    return [
        \Aero\Search\Elastic\Facet::create('stock', 'Stock Status', (int) $stock, $stock ? 'In-stock' : 'Out of stock'),
    ];
});
Re-index the documents after any modifications to the document structure.

How do I setup the search index?
Firstly, you should ensure that the Elasticsearch host information is defined in the store's .env. If you're running the service directly on your machine (either natively or through Docker), you'll most likely use the following details:
ELASTICSEARCH_HOST=localhost
ELASTICSEARCH_PORT=9200
When using a remote connection, ensure the host does not contain the port and URI scheme.
It is also important to ensure the STORE_IDENTIFIERis defined. This value is automatically added to the project's .envwhen using the Aero CLI tool to install and configure a store.
To create the Elasticsearch index and apply the mappings, run the following command within the root directory of your project:
php artisan aero:search:install

How do I re-build the listings and search index?
At times (especially during the development phase of a store build), it may be necessary to delete and documents and freshly index them. This process can be achieved using the following command:
php artisan aero:search:rebuild
Be careful running the re-build command on a production store.
Due to the nature of how this process runs, there will be a temporary period where there are no listings or products shown.

How do I extend the document structure for listings and seach?
One of the most useful features of the listings and search system is the ability to add custom data into the document. Typically, this will be data to be consumed in the code that forms the storefront listings page.
To extend a document, use the **add()**method, to define a closure that will return an arrayof data to merge into the document structure. This is typically done from within a ServiceProvider. If the code is unique to a particular store it could be placed in the boot method of the AppServiceProvider. If it is to be included as part of a module, then it can be added in the setup method of the module's ServiceProvider.
\Aero\Search\Elastic\Documents\ListingDocument::add('search-result', function ($document) {
    $colors = $document->getModel()->visibleVariants()->pluck('attributes')->flatten()->filter(function ($attribute) {
        return strpos(strtolower($attribute->group->name), 'color') !== false;
    })->pluck('name')->unique()->values()->all();
    return [
        'colors' => $colors,
    ];
});
The available section of the document that can be extended are:
search-result - search-data

How do I filter listings?
There may be occasions where you need to pre-filter listings. For example, you may want to ensure that all listings are connected to a certain sub-store or Dropship provider, or all share a tag that is set by your application rather than applied by the customer. Below is an example that extends the Elasticsearch listings query to scope the results to only those that have stock:
\Aero\Search\Elastic\Repositories\ElasticListings::extend(function ($listings) {
    $filter = new \Elastica\Query\Term(['number-sort.has-stock' => ['value' => 1]]);
    $listings->base()->getQuery()->addFilter($filter);
});

How to Extend the Elastic Search Listing Model
The 
Aero\Search\Elastic\Models\Listingmodel is built up from an array of data returned from elastic search. You can modify the model's attributes through the 
Aero\Search\Elastic\Pipelines\ListingAttributespipeline.
For example you can change every listing’s name to be “Example”.
It’s important to note that this pipeline is run for every listing that is loaded, everytime. If you use this approach to add a resource intensive calculation it’s important to take steps such as caching so performance isn’t impacted.
<?php
namespace Acme\MyModule;
use Aero\Common\Providers\ModuleServiceProvider;
use Aero\Search\Elastic\Pipelines\ListingAttributes;
class ServiceProvider extends ModuleServiceProvider
{
   public function setup()
   {
       ListingAttributes::extend(function ($attributes) {
           $attributes['name'] = 'Example';
           return $attributes;
       });
   }
}

Module Development
Module Development

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:linkin the root directoryof 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)
    },
}

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 migratein the root project directoryafter 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 adminand storesub-folders of 
resources/viewscan 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 Provideris 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 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/adminin the modules’ 
composer.jsonfile.
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 conventionsand 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/adminin the modules’ 
composer.jsonfile.
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 Providerensures 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 migratein 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 routeswhich only give access to routes provided the user is an administrator. The other is the store routeswhich affect the store/frontend side of the system.
Admin routes
There are two ways of setting upadmin 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 routeson the same level as the srcdirectory – for more information refer to "Anatomy of a custom module.phpfile in the routesdirectory. 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 AdminModulefacade 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.

Payment Drivers
Payment Drivers

What is the payment driver interface in my payment driver?
All payment gateway drivers must implement the 
Aero\Payment\Contracts\PaymentDriverinterface.
The 
Aero\Payment\PaymentDriverabstract class can be used as the starting point, which supports the 
ECOMoperation mode by default.
It's important to register the payment driver with the payment processor, which is used by the checkout and admin modules. This should be done in a 
ServiceProviderusing the following call:
\Aero\Payment\PaymentProcessor::registerDriver('my_gateway', \Acme\PaymentDriver::class);

What are the capturing methods in my payment driver?
here are two types of capturing methods:
Automatic– the payment is both authorised and captured when the customer clicks the pay button - Manual– the payment is authorised when the customer clicks the pay button, the funds are ring-fenced and captured at a later date

How do I cancel a payment in my payment driver?
If capturing of the payment is done at a later stage to the initial authorisation, the payment gateway should support the ability to cancel the transaction. The driver should therefore implement a 
cancelmethod.
The 
cancelmethod should return a 
Aero\Payment\Responses\PaymentResponseinstance.
public function cancel(Payment $payment)
{
    $response = new \Aero\Payment\Responses\PaymentResponse();
    // request to cancel the transaction using the 3rd party API
    $capture = AcmePaymentsInc::cancelTransaction($payment->reference);
    // check if the transaction status is "cancelled"
    if ($capture->status !== AcmePaymentsInc::STATUS_CANCELLED) {
        // set an error on the response
        $response->setError('The transaction could not be cancelled.');
        return $response;
    }
    // internally mark the aero payment as cancelled
    $payment->cancel();
    // mark the response as successful
    $response->setSuccessful(true);
    return $response;
}

How do I capture a payment in my payment driver?
If the gateway provides the option to defer capturing the payment (authorising and capturing are performed at different times), the 
capturemethod should be implemented. This captured amount may be equal to or less than the authorised amount, and is always passed to the method in the lowest form of currency (pennies, cents, etc.).
The 
capturemethod should return a 
Aero\Payment\Responses\PaymentResponseinstance.
public function capture(int $amount, Payment $payment)
{
    $response = new \Aero\Payment\Responses\PaymentResponse();
    // request to capture the transaction using the 3rd party API
    $capture = AcmePaymentsInc::captureTransaction($payment->reference, $amount);
    // check if the transaction status is "captured"
    if ($capture->status !== AcmePaymentsInc::STATUS_CAPTURED) {
        // update the status of the aero payment to "failed"
        $payment->update([
            'state' => \Aero\Payment\Models\Payment::FAILED,
        ]);
        // set an error on the response
        $response->setError('The transaction failed to be captured.');
        return $response;
    }
    // internally capture the amount for the payment
    $payment->capture([
        'amount' => $amount,
    ]);
    // mark the response as successful
    $response->setSuccessful(true);
    return $response;
}
Aero will attempt to auto-capture any authorised payments against an order when the order is marked as dispatched.

How do I refund a payment in my payment driver?
If the payment gateway provides the ability to externally make refund requests, the payment driver can use the 
Aero\Payment\SupportsRefundingtrait.
The 
refundmethod should then be implemented, which returns an instance of 
Aero\Payment\Responses\PaymentResponse.
public function refund(int $amount, Payment $payment)
{
    $response = new \Aero\Payment\Responses\PaymentResponse();
    // request to refund the transaction using the 3rd party API, with a given amount
    $capture = AcmePaymentsInc::refundTransaction($payment->reference, $amount);
     // check if the transaction status is "refunded"
    if ($capture->status !== AcmePaymentsInc::STATUS_REFUNDED) {
       // set an error on the response
        $response->setError('The transaction could not be refunded.');
        return $response;
    }
    // internally refund the amount for the aero payment
    $payment->refund([
        'amount' => $amount,
    ]);
    // mark the response as successful
    $response->setSuccessful(true);
    return $response;
}

What are operating modes in my payment driver?
There are two modes a gateway can operate:
ECOM– a standard ecommerce online transaction, typically originating from the storefront - 
MOTO– a transaction that is taken via telephone or through the mail, typically originating from the backend admin
As the processing environment differs when the customer isn't present (i.e. no 3D-secure for telephone orders), stores may require different merchant credentials for each mode.
ECOM mode
In order to indicate the gateway supports processing storefront orders, ensure the 
Aero\Payment\SupportsEcomModetrait is used.
MOTO mode
In order to indicate the gateway supports processing telephone and mail orders, ensure the 
Aero\Payment\SupportsMotoModetrait is used.

How do I support express checkout in my payment driver?
If the payment gateway stores customer information, this can be used to populate the contact information, billing and/or shipping address.
To specify that the payment gateway supports the express checkout flow, use the 
Aero\Payment\OffersExpressCheckouttrait.
Since the express flow may not always be selected, the payment gateway must indicate if the customer is using the express checkout. This could be achieved by storing the status in their session:
public function isExpress(): bool
{
    return session()->has('acme_using_express_checkout');
}

Can I restricting availability of my payment driver?
It is likely that a payment gateway could have restrictions on when it can be used. For example, a finance based provider may require a certain order total amount, be restricted by geographical location or the items being purchased.
To determine if the gateway should be offered for the customer's session, use the 
availableForCartmethod within the payment driver:
public function availableForCart(Cart $cart): bool
{
    $minAmount = setting('my_gateway.min_order_amount');
    if ($minAmount !== null) {
        return $cart->total()->inc()->value >= $minAmount;
    }
    return true;
}

How do I return a payment response in my payment driver?
To unify all payment gateway driver communication, an 
Aero\Payment\Responses\PaymentResponseclass is used. This common class must be returned from the 
register, 
complete, 
capture, 
refundand 
cancelmethods.
Setting the payment view
Many payment gateways use an iframe or JavaScript code to bridge the store with the gateway provider. In order to output this to the customer on the checkout, or telephone operator for MOTO payments, the 
PaymentResponsecan define a view to output. To do so, pass the view to the 
setViewmethod. If no view is set, a JSON response will be returned.
Passing data to the payment view
If there is data to be passed to the view or JSON response, the 
setDatamethod should be used.
Marking as successful
To indicate that the action carried out in the method was successful, then the response should be flagged as successful calling the 
setSuccessfulmethod on the 
PaymentResponse.
Adding error messages
If an error occurred during the action, these messages can be passed to the 
PaymentResponseusing the 
setErrormethod.
Redirecting the user
If further action is required where the user needs to be redirected to a different URL (to complete 3D-secure on the card issuers website for example), a redirect can be set using the 
setRedirectmethod.

How do I register the transaction in my payment driver?
When a payment method is selected that uses the gateway driver, the 
registermethod is called. Within this method, the bootstrapping of the transaction should be carried out. This may involve connecting to the payment gateway's API to register the transaction. For example, the gateway may need to know the total amount to request from the customer's card, along with where to redirect the browser once the transaction has been processed.
The 
registermethod should return a 
Aero\Payment\Responses\PaymentResponseinstance.
public function register()
{
    // make an API call to create the transaction with Acme Payments (the fake 3rd party gateway company)
    $request = AcmePaymentsInc::createTransaction([
        'merchant_reference' => $this->order->reference,
        'amount' => $this->order->total_rounded,
        'currency' => $this->order->currency_code,
        'billing_address' => [
            'line_1' => $this->order->billingAddress->line_1,
            'postcode' => $this->order->billingAddress->postcode,
            'country' => $this->order->billingAddress->country_code,
        ],
        'return_url' => route("payments-{$this->getMerchantMode()}.complete-payment"),
    ]);
    $response = new \Aero\Payment\Responses\PaymentResponse();
    // set the response as successful if the transaction status is "created"
    $response->setSuccessful($request->status === AcmePaymentsInc::STATUS_CREATED);
    // set the data to pass to the view
    $response->setData([
        'transaction_reference' => $request->reference,    
    ]);
    // set the view to use (this is typically an iframe or JavaScript)
    $response->setView("acme-payments::{$this->getMerchantMode()}-form");
    return $response;
}
The callback URL(s) may differ depending on the operating mode. If the driver supports both 
ECOMand 
MOTOmodes, the 
getMerchantModemethod can be used:
$request = AcmePaymentsInc::createTransaction([
    // ...
    'return_url' => route("payments-{$this->getMerchantMode()}.complete-payment"),
]);

How do I complete the transaction in my payment driver?
Once the payment gateway has handled the customer inputting their secure details (such as card information), they will most likely be redirected back to the retailer's store. On doing so, the 
completemethod is called on the payment driver, allowing for the transaction to be validated and capturing of the payment.
The 
completemethod should return a 
Aero\Payment\Responses\PaymentResponseinstance.
public function complete()
{
    $reference = $this->request->input('reference');
    // don't continue if a reference isn't provided
    abort_unless($reference, 404);
    $response = new \Aero\Payment\Responses\PaymentResponse();
    // the order total
    $amount = $this->order->total_rounded;
    // obtain the transaction from the 3rd party API
    $request = AcmePaymentsInc::getTransaction($reference);
    // validate if the currency matches
    if ($request->currency !== $this->order->currency_code) {
        $response->setError('Currency does not match.');
        return $response;
    }
    // validate if the amount matches
    if ($request->amount !== $this->order->total_rounded) {
        $response->setError('Total amount does not match.');
        return $response;
    }
    // validate if the transaction status is "authorized"
    if ($request->status !== AcmePaymentsInc::STATUS_AUTHORIZED) {
        $response->setError('The transaction was not authorized.');
        return $response;
    }
    // create the payment record in the aero database
    $payment = $this->order->payments()->updateOrCreate([
        'reference' => $reference,
    ], [
        'id' => (string) \Illuminate\Support\Str::uuid(),
        'payment_method_id' => $this->method->getKey(),
        'state' => \Aero\Payment\Models\Payment::AUTHORIZED,
        'amount' => $amount,
        'currency_code' => $this->order->currency->code,
        'exchange_rate' => $this->order->currency->exchange_rate,
        'merchant_mode' => $this->getMerchantMode(),
    ]);
    // request to capture the transaction using the 3rd party API
    $capture = AcmePaymentsInc::captureTransaction($reference, $amount);
    // check if the transaction status is "captured"
    if ($capture->status !== AcmePaymentsInc::STATUS_CAPTURED) {
        // update the status of the aero payment to "failed"
        $payment->update([
            'state' => \Aero\Payment\Models\Payment::FAILED,
        ]);
        // set an error on the response
        $response->setError('The transaction failed to be captured.');
        return $response;
    }
    // internally capture the amount for the payment
    $payment->capture([
        'amount' => $amount,
    ]);
    // mark the response as successful
    $response->setSuccessful(true);
    return $response;
}

How do I display payment icons on checkout in my payment driver?
To improve brand awareness of the payment provider during the checkout process, a label view can be used. The payment driver should implement the labelView method, which returns a string. This is typically a namespaced view that lives in the payment gateway module.
public function labelView(PaymentMethod $method): ?string
{
    return view('my_gateway::label', compact('method'));
}

How do I restrict the availability of my payment driver?
It is likely that a payment gateway could have restrictions on when it can be used. For example, a finance based provider may require a certain order total amount, be restricted by geographical location or the items being purchased.
To determine if the gateway should be offered for the customer's session, use the 
availableForCartmethod within the payment driver:
public function availableForCart(Cart $cart): bool
{
    $minAmount = setting('my_gateway.min_order_amount');
    if ($minAmount !== null) {
        return $cart->total()->inc()->value >= $minAmount;
    }
    return true;
}

Payment Gateways
Payment Gateways

What are the available payment gateways?
A cash payment method is provided out of the box, which allows developers to complete orders through the checkout without having to worry about installing and configuring a payment gateway.
The cash payment method should be disabled before the store is put live.
The following payment methods are built into the platform:
Cash - Gift Voucher - Paymentless (for when no payment is needed)
Supported Gateways
If you are a payment gateway interested in partnering with Aero, or the one you're looking for isn't listed below, then please contact us to discuss further.
Aero offers and maintains the following gateway integrations, with more being added all the time.
Some gateways also support additional functionality which is indicated by the icons below. You - or your retailer - should consult with the gateway regarding any extra requirements needed from them in order to use these features.
💾 Supports saving of card details
Customers can save their card details and re-use them on future orders. This is typically done via generating a payment token which identifies the card to the gateway, and no sensitive card information is collected or stored on your server.
➕ **Supports additional payments
**An Aero feature allowing you to collect additional payments via our MOTO flow for orders which have not been completed by the customer or that have been modified, leading to extra charges.
📞 Supports MOTO
MOTO orders are taken by mail or telephone and these drivers will allow you to submit "cardholder not present" payments through the Aero admin.
🔗 Supports Pay By Link
An Aero feature allowing you to email a link to your customer so that they can pay for their order online even if it was taken over the phone or had previously been abandoned
- - - - - - - - - - - - - - - 
- - - - - - 
Card Payments
Finance / BNPL
Adyen 💾Barclaycard ePDQBarclaycard Fuse 💾 ➕ 📞 🔗Barclaycard Smartpaycheckout.com 💾 📞DNA Payments 💾 📞Opayo PI 💾 ➕ 📞 🔗PayPal (including Advanced Credit / Debit Cards)SagePay (VSP Server) 📞 🔗Secure Trading (Trust Payments) 📞Stripe (including Klarna and Link) 💾 🔗Total Processing 💾 📞 🔗Tyl by NatWestWindcave 💾 📞Worldline 💾 📞Worldpay (Access / Worldwide Payment Gateway) 💾 📞 🔗
ClearpayDeko NewpayDuologiHumm GroupKlarnaOmni CapitalV12 FinanceOpen BankingBoodil 🔗
Apple Pay / Google Pay
Gateways supporting Apple Pay and Google Pay are as follows - please note additional integration such as domain verification and certificates, as well as Apple Pay and Google Pay accounts may be required in order to take payments through digital wallets.
Please ensure you are on the latest available version of your integration, and check the integration's README for further instructions on what is required.
Driver
Apple Pay
Google Pay
Adyen *
Yes
Yes
Barclaycard Fuse
Yes
Yes
checkout.com
Yes
Yes
DNA Payments *
Yes
Yes
Opayo
Yes *
Yes
Secure Trading
Yes
No
Stripe *
Yes
Yes
Total Processing
Yes
Yes
Tyl by NatWest *
Yes
Yes
Windcave
Yes
Yes
Worldline *
Yes
Yes
Worldpay *
Yes
Yes
Support provided by gateway without requiring separate Apple Pay / Google Pay account. If the asterisk appears alongside the gateway name, this means the gateway supports this for both.
Other Available Gateways
We also have integrations available for the following gateways but do not officially support them as they have been built by third parties or have been deprecated. You can still install and use them but we will be unable to offer assistance if you experience issues:
Biller / NOTYD (now only available in Belgium / Netherlands) - Payl8r (deprecated) - Zip (no longer supports UK merchants)
Need a payment gateway that isn't listed above?
See our "Introduction to payment drivers

How do I restrict payment methods?
There may be circumstances where a retailer wishes to restrict the payment methods that are available to a customer when checking out. Whilst a payment gateway may have restrictions, these may not be sufficient, or the limitations may need to apply to multiple methods.
The payment methods can be refined for a store, by adding a cart validator resolver, for example:
\Aero\Payment\Models\PaymentMethod::setCartValidator(function ($method, $cart) {
    if ($method->driver === 'cash' && $cart->total()->inc()->value > 1000) {
        return false;
    }
    return $method->getDriver()->validateForCart($cart);
});
Note that the validator falls back to the driver validation.

Php Traits
Php Traits

Aero\\Common\\Traits\\CanExtendFillable
This trait can be added to a model class. When added your model will have the ability to have fillables dynamically added. The trait adds the following methods:
static makeFillable Method
This method accepts a key for the attribute to make fillable.

Aero\\Common\\Traits\\CastsToJavaScript
This trait can be added to a model class. This trait provides a way of defining attributes that should be kept when casting to javascript. You can define these attributes in a protected $castsToJsarray. The trait adds the following methods:
static addCastToJs Method
This static method provides a way to dynamically add attributes to the models $castsToJsarray. It expects you to pass the attribute key to be added.
jsSerialize Method
This method returns the value of the attributesToJsmethod.
toJavaScript Method
This method returns the JSON value of the jsSerializemethod.
attributesToJs Method
This method returns an array of the attributes that should be casted to javascript.
getCastsToJs Method
This method returns the attribute keys that should be cast to javascript.

Aero\\Common\\Traits\\Sluggable
This trait can be added to a model class. The trait adds a relationship to Aero\Store\Models\Slugand helper methods for adding a slug to the model.

Aero\\Catalog\\Traits\\Taggable
This trait can be added to a model class. The trait adds the ability to add tags to the model. The trait adds the following methods:
tag Method
This method accepts a Aero\Catalog\Models\Tagand will attach the tag to the model without detaching any other tags.
tags Method
This method defines the Eloquent relationship with Aero\Catalog\Models\Tag.
untag Method
This method accepts a Aero\Catalog\Models\Tagand will detach the tag from the model without detaching any other tags.

Aero\\Common\\Traits\\CanHaveAdditionalAttributes
This trait can be added to a model class. When added your model will have the ability to have additional attributes. The trait adds the following methods:
additionals Method
This is the relationship method. This provides the relationship to Aero\Common\Models\AdditionalAttribute.
hasAdditional Method
This method accepts a key string parameter and will do a database query to return a boolean value if the additional attribute with the key you provided exists.
scopeHavingAdditional Method
This method is a Laravel local scope method that will allow you to scope a query on your module to require that the model has a specific additional attribute.
getAdditionalAttribute Method
This method accepts an additional attributes key and allows you to get an additional attributes value using its key.
saveAdditionalAttribute Method
This method accepts key and value parameters and will save the key and value you provide as an additional attribute.

Aero\\Catalog\\Traits\\HasPrices
This trait can be added to a model class. This trait adds prices to the model. The trait adds the following methods:
currentPrices Method
This method returns a collection of the current viable price based upon the date and time.
basePrice Method
This method returns the base price.
prices Method
This method defines the eloquent relationship with Aero\Catalog\Models\Price.
futurePrices Method
This method returns a collection of prices that are set for the future.

Aero\\Store\\Traits\\Seoable
This trait can be added to a model class. The trait adds a relationship to ​​Aero\Store\Models\Seoand helper methods for adding SEO data to the model.

Aero\\Responses\\Traits\\HasSections
This trait can be added to a response builder class. It adds useful methods for managing sections of the page. There are other traits (HasBottomSections, HasTopSections, and HasSidebarSections) in the same namespace that add more section helpers (these traits all require that the class uses the HasSections trait as well). The trait adds the following methods:
getSectionKey Method
This method defines the key that is used to store the sections data on the response builder (defaults to ‘sections’).
setSection Method
This method will set a section. It accepts a key and value and optionally a section key (the section key will default to the getSectionKeymethod).
setSections Method
This method will set the sections. It accepts an array of sections and optionally a section key (the section key will default to the getSectionKeymethod).
getSection Method
This method will return a specified section. It accepts a key for the section and optionally a section key (the section key will default to the getSectionKeymethod).
getSections Method
This method will return a collection of the sections. It optionally accepts a section key (the section key will default to the getSectionKeymethod).
addSection Method
This method accepts a key and value to add to the sections. Using this method will put your section at the top of the sections collection.
addSectionBefore Method
This method works the same way as the addSectionmethod but also accepts a key for the section that this new section should be added before.
addSectionAfter Method
This method works the same way as the addSectionmethod but also accepts a key for the section that this new section should be added after.
removeSection Method
This method accepts a key of the section to remove and optionally accepts a section key (the section key will default to the getSectionKeymethod).
hasSections Method
This method returns if there are sections. It optionally accepts a section key (the section key will default to the getSectionKeymethod).

Aero\\Admin\\Traits\\HasPermissions
This trait adds the ability to add permissions to a class easily. The trait adds the following methods:
permissions Method
This method accepts an array that will set the permissions required.
allowed Method
This method optionally accepts a user and will return if the user has the required permissions.

Aero\\Admin\\Traits\\IsExtendable
This trait can be added to a class. This trait can be used on Pipelines (more information "introduction to pipelines

Pipelines
Pipelines

What are the available pipelines
Class
Description
Payload
CartItemBuilder
The process that occurs when adding an item to the cart. Extending this class allows for manipulation of the cart item before it is saved to the cart.
Aero\Cart\CartItem $item
ContentForHead
The HTML content that will be inserted into the  tag of the page.
String $content
ContentForBody
The HTML content that will be inserted just before the closing  tag of the page.
String $content
RobotsTxt
The robots.txt file. For more information, click here.
Illuminate\Support\Collection $content

How do I extend pipelines?
The payload data can be altered using the **extend()**method. This is typically done from within a Service Provider. If the code is unique to a particular store it could be placed in the **boot()**method of the AppServiceProvider. If it is to be included as part of a module, then it can be added to the setup method of the module's Service Provider.
For example, if a module needs to add an option to items to flag them as requiring gift wrap, the CartItemBuilderpipeline class should be extended in the module's ServiceProvideras shown below:
<?php
namespace Acme\MyModule;
use Aero\Cart\CartItemOption;
use Aero\Common\Providers\ModuleServiceProvider;
use Aero\Store\Pipelines\CartItemBuilder;
class ServiceProvider extends ModuleServiceProvider
{
    public function setup() 
    {
        CartItemBuilder::extend(function ($item) {
            if (request()->has('gift_wrap')) {
                $option = CartItemOption::create('Gift wrap');
                $item->addOption($option);
            }
        });
    }
}
To adjust a payload which is a simple string, ensure that the payload variable is passed by reference so that it can be directly modified:
\Aero\Store\Pipelines\ContentForHead::extend(function (&$content) {
    $content .= "";
    // or ...
    $content .= view('my_module::script-snippet');
});

Resource Lists
Resource Lists

How do I create a custom bulk action?
If we wish to add a new bulk action to a screen within the admin, all we have to do is create the bulk action using the BulkActionfacade. The bulk action also requires a specific Resource List, for example for Products.
BulkAction::create(BulkActionClass::class, ResourceList::class)
        ->permissions('bulkaction.permission')
        ->title('Title');
The above code would be instantiated within a Service Provider, whether it is a module service provider or the AppServiceProvider.
Example:
If we were to create a new bulk action for exporting products, we would first need to create a class extending BulkActionJob:
BulkAction::create(ExportProducts::class, ProductsResourceList::class)
        ->notRunnable()
        ->permissions('products.export')
        ->title('Export products');
Properties
Bulk Actions have a number of properties, each responsible for affecting a different aspect of the bulk action.
Wide Content
We can choose to display the contents of our bulk action in a wider container, which can prove to be more useful for certain modules. In order to display its contents in a container stretched to the width of the screen, apply the following function to the facade while creating it:
BulkAction::create(BulkActionClass::class, ResourceList::class)
        ->wideContent()
        ->permissions('bulkaction.permission')
        ->title('Title');
Runnable & Not Runnable
Not every bulk action is the same, and it’s possible to remove the Runbutton from the bulk action in order to replace it with a custom button, for example calling the API. In order to remove the built-in Runbutton for a particular bulk action, apply the following function to the facade while creating it:
BulkAction::create(BulkActionClass::class, ResourceList::class)
        ->notRunnable()
        ->permissions('bulkaction.permission')
        ->title('Title');
Confirm
We can set the bulk action to prompt the user before performing its intended action by simply chaining a method to the BulkAction creation:
BulkAction::create(BulkActionClass::class, ResourceList::class)
        ->confirm()
        ->permissions('bulkaction.permission')
        ->title('Title');
The prompt will now ask the user if they are sure of their selection.
Title
We can set a custom title/main heading for the bulk action content, this can be done by chaining a function to the facade we use for creating the bulk action, e.g.:
BulkAction::create(BulkActionClass::class, ResourceList::class)
        ->title('Your title')
        ->permissions('bulkaction.permission')
        ->title('Title');
View
Aero has the ability to display a specific view when it performs the bulk action, this can be done by chaining a function to the facade we use for creating the bulk action, e.g.:
BulkAction::create(BulkActionClass::class, ResourceList::class)
        ->view(‘namespace::bulk.form’)
        ->permissions('bulkaction.permission')
        ->title('Title');

What are the default resource lists?
The following classes are in the Aero\Admin\ResourceListsnamespace and any class that doesn’t have a view uses the admin::resource-lists.indexview.
Class
View
AttributeGroupsResourceList
–
BlocksResourceList
–
CategoriesResourceList
admin::catalog.categories.index
CollectionsResourceList
–
CombinationsResourceList
–
CountriesResourceList
–
CurrenciesResourceList
–
CustomerGroupsResourceList
–
CustomersResourceList
–
CustomerTaxGroupsResourceList
–
DiscountsResourceList
–
FulfillmentMethodsResourceList
–
FulfillmentsResourceList
–
MailNotificationsResourceList
–
ManufacturersResourceList
–
MissingPagesResourceList
–
OrdersResourceList
–
OrderStatusesResourceList
–
PagesResourceList
–
PaymentMethodsResourceList
–
PriceListsResourceList
–
ProductsResourceList
admin::catalog.products.index
ProductTaxGroupsResourceList
–
RedirectsResourceList
admin::content.redirects.index
ShippingMethodsResourceList
–
SubscriptionPlansResourceList
–
SubscriptionsResourceList
–
TagGroupsResourceList
–
TaxRatesResourceList
–
TaxRulesResourceList
–
UsersResourceList
–
VouchersResourceList
–

How do I create my own resource list?
To create a resource list you need a normal Laravel controller and a class that extends Aero\Admin\ResourceListsAbstractResourceList.
<?php
namespace Acme\MyModule\ResourceLists;
use Aero\Admin\ResourceLists\AbstractResourceList;
class OrdersResourceList extends AbstractResourceList
{
   //
}
Your controller should be like this example but for your new resource list:
<?php
namespace Acme\MyModule\Http\Controllers;
use Aero\Admin\Http\Controllers\Controller;
use Aero\Admin\ResourceLists\OrdersResourceList;
use Illuminate\Http\Request;
class OrdersController extends Controller
{
   public function index(OrdersResourceList $list, Request $request)
   {
       return view('admin::resource-lists.index', [
           'list' => $list = $list(),
           'results' => $list->apply($request->all())
               ->with('status', 'customer', 'items', 'currency')
               ->paginate($request->input('per_page', 24) ?? 24),
       ]);
   }
}
Adding Columns to your Resource List
To add columns to your resource list you need to implement the protected **columns()**method and make it return an array of your columns. The entries in the array should use the Aero\Admin\ResourceLists\ResourceListColumn::createhelper method. This method expects a string parameter for the columns heading, a closure parameter for how to render the columns data (this can return a string), and an optional string parameter for the columns key (by default this is generated from the columns header).
<?php
namespace Acme\MyModule\ResourceLists;
use Aero\Admin\ResourceLists\AbstractResourceList;
use Aero\Admin\ResourceLists\ResourceListColumn;
class OrdersResourceList extends AbstractResourceList
{
   protected function columns(): array
   {
       return [
           ResourceListColumn::create('Order', function ($row) {
               return view('admin::resource-lists.link', [
                   'route' => route('admin.orders.view', array_merge(request()->all(), ['order' => $row])),
                   'text' => '#'.$row->reference,
               ])->render();
           }),
       ];
   }
}
Making Columns Searchable
You can use the setSearchActionmethod to define how your column is searched. This method accepts a closure (that is passed the query and search term) that defines how the search should happen and an optional string for the text to show in the search field dropdown (this is column header by default).
ResourceListColumn::create('Order', function ($row) {
   return view('admin::resource-lists.link', [
       'route' => route('admin.orders.view', array_merge(request()->all(), ['order' => $row])),
       'text' => '#'.$row->reference,
   ])->render();
})
->setSearchAction(function ($query, $term) {
   $searchTerm = ltrim($search, '#');
  
   $query->whereLower('reference', 'like', "%{$searchTerm}%");
}, 'Order Reference')
Making Columns Invisible
You can use the invisiblemethod to make your column invisible. The main reason for wanting to do this is to add a search for a column that shouldn’t be rendered.
ResourceListColumn::create('Order', function ($row) {
   return view('admin::resource-lists.link', [
       'route' => route('admin.orders.view', array_merge(request()->all(), ['order' => $row])),
       'text' => '#'.$row->reference,
   ])->render();
})
->invisible()
Handling Searching for your Resource List
When your resource list is searched without a specific column being selected a **handleSearch()**function will be called to handle the search. The method will be passed the search term and you are able to use $this->queryto access the query.
<?php
namespace Acme\MyModule\ResourceLists;
use Aero\Admin\ResourceLists\AbstractResourceList;
class OrdersResourceList extends AbstractResourceList
{
   protected function handleSearch($search)
   {
       $searchTerm = ltrim($search, '#');
       $this->query->whereLower('reference', 'like', "%{$searchTerm}%");
   }
}
Adding Row Views to your Resource List
Row views are additional rows rendered after each complete row (the best example of this is how the order items are listed after each row of the order on the orders page). To add a row view you simply need to provide your view in the protected $rowViewsarray.
<?php
namespace Acme\MyModule\ResourceLists;
use Aero\Admin\ResourceLists\AbstractResourceList;
class OrdersResourceList extends AbstractResourceList
{
   protected $rowViews = [
       'admin::resource-lists.orders.items',
   ];
}
Adding Buttons to your Resource List Header
Most resource list pages will require a create button. You can easily add buttons to the top of your resource list page by defining a header slot. Once you have defined a header slot you can inject your view using the admin slots helper.
<?php
namespace Acme\MyModule\ResourceLists;
use Aero\Admin\ResourceLists\AbstractResourceList;
class OrdersResourceList extends AbstractResourceList
{
   protected $headerSlot = 'my-list.index.header.buttons';
}
Adding Admin Filters to your Resource List
First you will need to create an Admin Filter class as shown in the artile "How do I create a custom admin filter?
To register your newly created admin filter with your resource list you need to ensure that your admin filter class is in the resource lists protected static $filters array.
<?php
namespace Acme\MyModule\ResourceLists;
use Aero\Admin\Filters\Order\OrderStatusAdminFilter;
use Aero\Admin\ResourceLists\AbstractResourceList;
class OrdersResourceList extends AbstractResourceList
{
   protected $filters = [
       OrderStatusAdminFilter::class,
   ];
}
Adding Sort Bys to your Resource List
To add sort bys to your resource list you need to implement the protected **sortBys()**method and make it return an array of your sort bys. The entries in the array should use the **Aero\Admin\ResourceLists\ResourceListSortBy::create()**helper method. This method expects an array parameter holding the dropdown options that will be shown in the frontend dropdown, and a closure that will be executed when the sort option is active.
<?php
namespace Acme\MyModule\ResourceLists;
use Aero\Admin\ResourceLists\AbstractResourceList;
use Aero\Admin\ResourceLists\ResourceListSortBy;
class OrdersResourceList extends AbstractResourceList
{
   protected function sortBys(): array
   {
       return [
           ResourceListSortBy::create([
               'order-az' => 'Order A to Z',
               'order-za' => 'Order Z to A',
           ], function ($sortBy, $query) {
               return $sortBy === 'order-az' ? $query->orderBy('reference') : $query->orderByDesc('reference');
           }),
       ];
   }
}
If you would like your sort by to be enabled by default then you call pass null for the first parameter like this:
<?php
namespace Acme\MyModule\ResourceLists;
use Aero\Admin\ResourceLists\AbstractResourceList;
use Aero\Admin\ResourceLists\ResourceListSortBy;
class OrdersResourceList extends AbstractResourceList
{
   protected function sortBys(): array
   {
       return [
           ResourceListSortBy::create(null, function ($sortBy, $query) {
               return $query->orderByRaw('coalesce(`orders`.`ordered_at`, `orders`.`created_at`) desc');
           }),
       ];
   }
}

How do I extend an exisiting resource list?
Adding a New Column
To add columns to a resource list you can use the addColumn, addColumnBefore, or addColumnAftermethods from within the pipeline. These methods return a usual resource list column which allows you to chain on the other available column methods.
addColumn Method
This method accepts a column header and a closure that defines how the column will be rendered. A column added with this method will be added as the first column.
addColumnBefore Method
This method accepts the same parameters as the addColumnmethod but additionally accepts a third parameter, the key of the column this new column should be added before.
addColumnAfter Method
This method accepts the same parameters as the addColumn method but additionally accepts a third parameter, the key of the column this new column should be added after.
Finding the Current Column Keys
If you do not know the keys of the current columns you can use this code snippet to dump the current keys out.
<?php
namespace Acme\MyModule;
use Aero\Admin\ResourceLists\OrdersResourceList;
use Aero\Common\Providers\ModuleServiceProvider;
class ServiceProvider extends ModuleServiceProvider
{
   public function setup()
   {
       dd((app(OrdersResourceList::class)())->getColumns()->map->key());
   }
}
<?php
namespace Acme\MyModule;
use Aero\Admin\ResourceLists\OrdersResourceList;
use Aero\Common\Providers\ModuleServiceProvider;
class ServiceProvider extends ModuleServiceProvider
{
   public function setup()
   {
       OrdersResourceList::extend(function (OrdersResourceList $list) {
           $list->addColumn('My Column', function($row) {
               return 'Example';
           });
           $list->addColumnBefore('Before Ref', function($row) {
               return $row->reference;
           }, 'order');
           $list->addColumnAfter('After Ref', function($row) {
               return $row->reference;
           }, 'order');
       });
   }
}
Adding a New Filter
To add a filter to a resource list you can use the addFiltermethod from within the pipeline. This method expects you to pass an admin filter class. You can create an admin filter class as shown in the article "How do I create a custom Admin Filter?
<?php
namespace Acme\MyModule;
use Acme\MyModule\Filters\CreatedAtAdminFilter;
use Aero\Admin\ResourceLists\OrdersResourceList;
use Aero\Common\Providers\ModuleServiceProvider;
class ServiceProvider extends ModuleServiceProvider
{
   public function setup()
   {
       OrdersResourceList::extend(function (OrdersResourceList $list) {
           $list->addFilter(CreatedAtAdminFilter::class);
       });
   }
}
Adding a New Sort By
To add a sort by to a resource list you can use the addSortBymethod from within the pipeline. This method expects you to pass a Aero\Admin\ResourceLists\ResourceListSortBy. You can create a resource list sort by class as shown in the article "How do I create my own Resource List?
<?php
namespace Acme\MyModule;
use Aero\Admin\ResourceLists\OrdersResourceList;
use Aero\Admin\ResourceLists\ResourceListSortBy;
use Aero\Common\Providers\ModuleServiceProvider;
class ServiceProvider extends ModuleServiceProvider
{
   public function setup()
   {
       OrdersResourceList::extend(function (OrdersResourceList $list) {
           $list->addSortBy(ResourceListSortBy::create([
               'latest' => 'Latest',
               'oldest' => 'Oldest',
           ], function ($sortBy, $query) {
               return $sortBy === 'oldest' ? $query->orderBy('created_at') : $query->orderByDesc('created_at');
           }));
       });
   }
}

What is a bulk action?
A bulk action allows for the selection of multiple values from a list, and apply some logic to items that have only been selected. Bulk actions exist for many different Resource Lists, which is what is used for determining items that have been selected.
Bulk actions are usually dispatched as a job, given the bulk nature of the action, which carries out tasks in a sequential manner. There are exceptions to this rule however and Aero can also return responses straight to the browser, e.g. downloading all selected content blocks as a .zip.

How do I add a custom column to the product list?
As the products list is a resource list you can add a custom column like this:
More information on resource lists can be found in the article "How do I extend an existing Resource List?
<?php
namespace Acme\MyModule;
use Aero\Admin\ResourceLists\ProductsResourceList;
use Aero\Common\Providers\ModuleServiceProvider;
class ServiceProvider extends ModuleServiceProvider
{
   public function setup()
   {
       ProductsResourceList::extend(function (ProductsResourceList $list) {
           $list->addColumn('My Column', function ($row) {
               return $row->id;
           });
       });
   }
}

How do I add a custom column to the order list?
As the orders list is a resource list you can add a custom column like this:
More information on resource lists can be found in the article "How do I extend an existing Resource List?
<?php
namespace Acme\MyModule;
use Aero\Admin\ResourceLists\OrdersResourceList;
use Aero\Common\Providers\ModuleServiceProvider;
class ServiceProvider extends ModuleServiceProvider
{
   public function setup()
   {
       OrdersResourceList::extend(function (OrdersResourceList $list) {
           $list->addColumn('My Column', function ($row) {
               return '#'.$row->reference;
           });
       });
   }
}

Response Builders
Response Builders

How do I access the properties of the response builder?
All response builders hold properties that were assigned to them upon creation from the request. These properties are typically the arguments that would be passed to a conventional Laravel controller and subsequently used in the code within the controller.
For example on the ProductPage, the Aero\Catalog\Models\Product modelfor the requested URL can be accessed as follows:
\Aero\Store\Http\Responses\ProductPage::extend(function ($page) {
    $product = $page->product;
    // do things with the $product model
});

How do I pass data to a response?
The most common task performed when extending responses, is setting additional data. Typically, for HTML responses that render from a view, this involves sending data to the view to be used as a variable. If the response returns JSON, it may be adding additional key/value pairs.
For example, if you needed to obtain customer reviews for a given product and output them on the product page, you could do the following, which passes a product to a 3rd party reviews API and then adds the reviews it returns to the response data, which is then passed to the product.twig view file ready to be loop around and outputted.
\Aero\Store\Http\Responses\ProductPage::extend(function ($page) {
    $reviews = \Acme\ReviewsAPI::forProduct($page->product);
    $page->setData('reviews', $reviews);
});
Another example is getting the latest 5 blog posts from a blog module and sending them to the homepage to be displayed:
\Aero\Store\Http\Responses\Homepage::extend(function ($page) {
    $posts = \Acme\BlogPosts::latest()->limit(5)->get();
    $page->setData('posts', $posts);
});
You can obtain the existing data set against the response builder using the **getData()**method and remove data using the **removeData()**method.

How do I redirect a response?
Sometimes you may wish to redirect the user to another location, either to an external site or within the store. To do so, pass the URL, route(), or an Illuminate\Http\RedirectResponseinstance to the **setRedirect()**method. For example, if you stored the age of customers (computed from their provided date of birth), and needed to prevent access to those under the age of 18:
\Aero\Store\Http\Responses\ProductPage::extend(function ($page) {
    if ($page->request->customer()->age < 18) {
        $page->setRedirect('/denied');
    }
});
As extension code is evaluated in the order it's added to the response builder, code that runs after this step may override the redirect. To force a redirect and prevent execution of the code that follows in the pipeline, you should return an instance of the **redirect()**helper:
\Aero\Store\Http\Responses\ProductPage::extend(function ($page) {
    if (! $page->product->canBeViewedByIp($page->request->ip())) {
        return redirect('/denied');
    }
});

How do I set the response status code?
There may be situations where the HTTP response status needs to be altered. By default, the returned status is 200, however, this can be changed by passing the new status code to the **setStatus()**method:
\Aero\Store\Http\Responses\ProductPage::extend(function ($page) {
    $page->setStatus(403);
});

How do I set response headers?
When extending the response builder, you can set HTTP headers to be sent with the response:
\Aero\Store\Http\Responses\ProductPage::extend(function ($page) {
    $page->setHeader('foo', 'bar');
});

How do I attach middlewear to a response builder?
Since Aero does not expose the underlying routes and controllers, middleware can be attached directly to the response builder. This allows for code to be executed before the main pipeline is run.
For example, to restrict all products from being accessible to guest visitors, you could register the authmiddleware to the ProductPageresponse:
\Aero\Store\Http\Responses\ProductPage::middleware('auth');
When assigning middleware, you can also pass the fully qualified class name:
\Aero\Store\Http\Responses\ProductPage::middleware(\App\Http\Middleware\CheckAge::class);
Alternatively, you can provide a Closure. For example, middleware could be added to the homepage to set a tracking cookie from the referrer:
\Aero\Store\Http\Responses\Homepage::middleware(function ($request, $next) {
    return tap($next($request), function ($response) use ($request) {
        $response->cookie('referer', $request->header('Referer'));
    });
});
Refer to the Laravel documentation for more information on middleware.

How do I extend responses?
Adding additional code to run on the response is as simple as calling the **extend()**method on the response builder class. This is typically done from within a Service Provider. If the code is unique to a particular store it could be placed in the boot method of the AppServiceProvider. If it is to be included as part of a module, then it can be added to the setup method of the module's Service Provider.
For example, if a module needs to send a variable footo the product.twigview file for it to be outputted in the HTML, the ProductPageresponse builder class should be extended in the module's Service Provideras shown below:
<?php
namespace Acme\MyModule;
use Aero\Common\Providers\ModuleServiceProvider;
use Aero\Store\Http\Responses\ProductPage;
class ServiceProvider extends ModuleServiceProvider
{
    public function setup() 
    {
        ProductPage::extend(function ($page) {
            $page->setData('foo', 'bar');
        });
    }
}
When passing a Closureto the extend method, the $pageargument is the response builder instance that will process the response.
For more advanced use cases, you can pass the response builder on for further processing in order to return the Illuminate\Http\Responseinstance. This is useful in situations when you may wish to modify the response before it is passed to the browser. A common use case for this is to add cookies to the response:
\Aero\Store\Http\Responses\ProductPage::extend(function ($page, $next) {
    $response = $next($page);
    $response->cookie('foo', 'bar');
    return $response;
});
Alternatively, a fully qualified class name can be passed to the **extend()**method, which reduces bloat in the Service Providerand provides a better indication of what the code is responsible for. When doing so, the class must implement a **handle()**method:
<?php
namespace App\Http\Extensions;
class AddFooVariableToProductPage
{
    public function handle($page)
    {
        $page->setData('foo', 'bar');
    }
}
\Aero\Store\Http\Responses\ProductPage::extend(AddFooVariableToProductPage::class);
Note that this code will only evaluate when Aero routes a request to the particular response builder, i.e. code that is set to run on the ProductPagewill not be processed on a request for the Homepage.

How do I change the response view?
For responses that return HTML, a Twig view file is used. Whilst these are originally defined, the view can be changed. For example, to change the product page view based on product data:
\Aero\Store\Http\Responses\ProductPage::extend(function ($page) {
    if ($view = $page->product->additional('view')) {
        $page->setView($view);
    }
});
Setting the view to nullwill result in a JSON response.
You can get the current view for a response using $page->getView().

What are the available responses from the response builder?
Core
Class
Description
View
Homepage
The homepage of the store.
homepage.twig
ProductPage
The product page providing information on a particular product.
product.twig
ListingsPage
The listings page which displays available products for the particular criteria defined against the URL
listings.twig
ListingsJson
The JSON response of listings for the particular criteria defined against the URL.
-
SearchPage
The search results for a particular search term.
search.twig
SearchJson
The JSON response of search results for a particular search term.
-
CartPage
The cart summary page, where customers can see an overview of the items in their cart and update quantities.
cart.twig
CartItemAdd
Actions to take when an item is added to the cart.
-
CartItemUpdate
Actions to take when an item is updated in the cart.
-
CartEmpty
Actions to take when the cart is emptied.
-
CartDiscountCodeApply
Actions to take when a discount code is applied to the cart.
-
CartDiscountCodeRemove
Actions to take when a discount code is removed from the cart.
InformationPage
A static page used to display information, for example the About Us or Terms & Conditions.
information-page.twig
Error404Page
The 404 page that displays when a page does not exist.
errors/404.twig
FormSubmit
Actions to take when a form is submitted.
-
AccountOverviewPage
A static page used to display account overview information.
account/account-overview.twig
Admin
Catalog
Class
Description
View
AdminDashboardPage
Used to display the admin dashboard.
admin/dashboard.blade.php
AdminCategoryCreatePage
Used to display the category creation page in the admin.
admin/catalog/categories/new.blade.php
AdminCategoryEditPage
Used to display the category edit page in the admin
admin/catalog/categories/edit.blade.php
AdminCategoryStore
Actions for creating a new category.
-
AdminCategoryUpdate
Actions for updating an existing category.
-
AdminCollectionCreatePage
Used to display the collection creation page in the admin.
admin/catalog/collections/index.blade.php
AdminCollectionEditPage
Used to display the collection edit page in the admin.
admin/catalog/collection/edit.blade.php
AdminCollectionStore
Actions for creating a new collection.
-
AdminCollectionUpdate
Actions for updating an existing collection.
-
AdminManufacturerCreatePage
Used to display the manufacturer creation page in the admin.
admin/catalog/manufacturers/new.blade.php
AdminProductCreatePage
Used to display the product creation page in the admin.
admin/catalog/products/new.blade.php
AdminProductStore
Action for creating a new product.
-
AdminProductUpdate
Actions for updating an existing product.
-
Discounts
Class
Description
View
AdminDiscountCreatePage
Used to display the discount creation page in the admin.
AdminDiscountEditPage
Used to display the discount edit page in the admin.
admin/discounts/edit.blade.php
AdminDiscountStore
Actions for creating a new discount.
-
AdminDiscountUpdate
Actions for updating an existing discount.
-
Orders
Class
Description
View
AdminOrderViewPage
Used to display the order view page in the admin.
admin/orders/view.blade.php

Settings
Settings

What are the settings available to developers?
Settings allow you to add dynamic values to your modules that can be modified by admin users easily through the admin.
The values of your settings (unless the value is the default you set) are stored in json files inside of your storage directory.
└─ storage
    └─ app
        └─ settings

Do do I set the default value of my custom setting?
You can add a default value to any setting by using the defaultmethod and passing your default value.
<?php
namespace Acme\MyModule;
use Aero\Common\Facades\Settings;
use Aero\Common\Providers\ModuleServiceProvider;
use Aero\Common\Settings\SettingGroup;
class ServiceProvider extends ModuleServiceProvider
{
   public function setup()
   {
       Settings::group('acme-my-module', function (SettingGroup $group) {
           $group->string('denied_message')->default('Access Denied');
           $group->array('allow_list')->default(['admin']);
           $group->boolean('allow_list_enabled')->default(true);
       });
   }
}

How do I register custom settings?
To register settings you need to use the Aero\Common\Facades\Settingsfacade from the setupmethod of your modules service provider. You should call the groupmethod on the facade and pass in a unique name for your settings group and a closure that defines your settings group.
<?php
namespace Acme\MyModule;
use Aero\Common\Facades\Settings;
use Aero\Common\Providers\ModuleServiceProvider;
use Aero\Common\Settings\SettingGroup;
class ServiceProvider extends ModuleServiceProvider
{
   public function setup()
   {
       Settings::group('acme-my-module', function (SettingGroup $group) {
           $group->string('denied_message');
           $group->array('allow_list');
           $group->boolean('allow_list_enabled');
       });
   }
}

How do I access a custom setting?
You can access a setting by using the settingfunction that is available in PHP, Blade, and Twig. You simply need to pass your setting group name, a full stop, and then the settings name.
setting(‘acme-my-module.allow_list’)

What are the available setting types?
Setting
Description
$group->array(‘allow_list’);
Returns an array.
$group->boolean(‘enabled’);
Returns a boolean, true or false.
$group->eloquent('order_status', OrderStatus::class);
Stores the key of the eloquent model you pass and returns an instance of the model.
$group->encrypted('api_key');
Stores a string encrypted and returns the string decrypted.
$group->float('min_value');
Returns a number as a float.
$group->integer('per_page');
Returns a number as an integer.
$group->string(‘message’);
Returns a string.
$group->date('go_live');
Returns a Carbon date instance.
$group->dateRange('sale_period');
Returns an array with start and end keys that hold a Carbon date instance.

How do I configure the admin edit page?
By default your setting group is editable through the admin by a generated edit page. You cannot remove this generated edit page but you can stop users from being able to edit the settings using it. It’s important to note that if you disable editing through this edit page that users can still edit the settings manually through the storage json file.
To make your setting group not editable you should call the notEditablemethod when defining your group.
<?php
namespace Acme\MyModule;
use Aero\Common\Facades\Settings;
use Aero\Common\Providers\ModuleServiceProvider;
use Aero\Common\Settings\SettingGroup;
class ServiceProvider extends ModuleServiceProvider
{
   public function setup()
   {
       Settings::group('acme-my-module', function (SettingGroup $group) {
           $group->string('denied_message');
           $group->array('allow_list');
           $group->boolean('allow_list_enabled');
           $group->notEditable();
       });
   }
}
Validation Rules
These are helper methods that map to their equivalent Laravel Validation Rules
Rule
Works For
$group->string('key')->required();
All
$group->string('key')->between($min, $max);
String, Integer, Float, Array, Encrypted
$group->string('key')->min($min);
String, Integer, Float, Array, Encrypted
$group->string('key')->max($max);
String, Integer, Float, Array, Encrypted
$group->string('key')->size($size);
String, Integer, Float, Array, Encrypted
$group->string('key')->greaterThan($size);
String, Integer, Float, Array, Encrypted
$group->string('key')->lessThan($size);
String, Integer, Float, Array, Encrypted
$group->string('key')->in($needles);
String, Integer, Float
$group->string('key')->email();
String, Encrypted
$group->string('key')->uuid();
String, Encrypted
$group->string('key')->url();
String, Encrypted
$group->string('key')->ip();
String, Encrypted
$group->string('key')->startsWith($needle);
String, Encrypted
$group->string('key')->startsWithOneOf($needles);
String, Encrypted
$group->float('key')->step($step);
Float
$group->date('key')->beforeToday();
Date
$group->date('key')->before($date);
Date
$group->date('key')->afterToday();
Date
$group->date('key')->after($date);
Date
Methods
Method
For
Description
$group->title($title);
-
Set the title shown in the list of settings (by default this is generated from the group name).
$group->summary($summary);
-
Set the summary shown in the list of settings.
$group->notEditable();
-
Disable editing through the generated admin page.
$group->string('key')->label($label);
All
Set the label for the edit field (by default this is generated from the settings key).
$group->string('key')->hint($hint);
All
Add a helpful hint to the edit field.
$group->string('key')->textarea();
String
Make the string field appear as a textarea.
$group->string('key')->wysiwyg();
String
Make the string field appear as a wysiwyg.
$group->eloquent('order_status', OrderStatus::class)->searchField($field);
Eloquent
Define the field used for searching in the searchable select.
$group->eloquent('order_status', OrderStatus::class)->searchFields($field);
Eloquent
Define multiple fields used for searching in the searchable select (first field is the value).
$group->array('key')->associative();
Array
Define the array as an associative array - an array that has a key and value.
$group->array('key')->definition($definition);
Array
Set a custom definition for your array (link:section below this).
Complex Array Definitions
If you have a complex array (an array of arrays that have multiple keys), you need to define the contents of the array and any validation for each key.
Here is a code example that defines an additional_checkboxes array that is an array of arrays. The child arrays have a text key (that has validation ensuring it is a string and required) and a required key (that has validation ensuring it is a boolean).
Although this example uses the string, required, and boolean rules you can use any Laravel Validation rule
<?php
namespace Acme\MyModule;
use Aero\Common\Facades\Settings;
use Aero\Common\Providers\ModuleServiceProvider;
use Aero\Common\Settings\SettingGroup;
class ServiceProvider extends ModuleServiceProvider
{
   public function setup()
   {
       Settings::group('acme-my-module', function (SettingGroup $group) {
           $group->array('additional_checkboxes')->definition([
               'text' => ['string', 'required'],
               'required' => ['boolean']
           ]);
       });
   }
}

How do I handle translations?
When defining your setting groups title/summary, or a settings label/hint you can make use of Laravel Localization for translations.
<?php
namespace Acme\MyModule;
use Aero\Common\Facades\Settings;
use Aero\Common\Providers\ModuleServiceProvider;
use Aero\Common\Settings\SettingGroup;
class ServiceProvider extends ModuleServiceProvider
{
   public function setup()
   {
       Settings::group('acme-my-module', function (SettingGroup $group) {
           $group->title(__('acme-my-module.title'));
           $group->summary(__('acme-my-module.summary'));
           $group->string('message')
               ->label(__('acme-my-module.labels.message'))
               ->hint(__('acme-my-module.hints.message'));
       });
   }
}

How do I add settings to the product model?
Using the same syntax as when you create settings
<?php
namespace Acme\MyModule;
use Aero\Catalog\Models\Product;
use Aero\Common\Providers\ModuleServiceProvider;
use Aero\Common\Settings\SettingGroup;
class ServiceProvider extends ModuleServiceProvider
{
   public function setup()
   {
       Product::settings('my-module', function (SettingGroup $group) {
           $group->encrypted('password');
           $group->boolean('require_password_to_view')->default(false);
       });
   }
}
To access your settings value you need to use the settings method on the product model, passing in the key for the setting you want.
<?php
namespace Acme\MyModule;
use Aero\Catalog\Models\Product;
use Aero\Common\Providers\ModuleServiceProvider;
use Aero\Common\Settings\SettingGroup;
class ServiceProvider extends ModuleServiceProvider
{
   public function setup()
   {
       Product::settings('my-module', function (SettingGroup $group) {
           $group->encrypted('password');
           $group->boolean('require_password_to_view')->default(false);
       });
       $product = Product::first();
       $password = $product->settings('my-module.password');
       dd($password);
   }
}

Validation
Validation

How do I create custom validation requests?
To create a custom validation request, we need to extend the AeroRequestabstract class that’s located in aerocommerce/core.
To create a custom request, open the terminal in the project root directory and type the following command:
php artisan make:request MyRequest
The above command generates a class file that we now have to modify to suit the needs of both AeroRequestand the data we’re working with. The first change is the class our brand new request extends, instead of FormRequest, we now need the class to extend AeroRequestfrom namespace App\Http\Requests.
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class MyRequest extends FormRequest
{
    /**
     * Determine if the user is authorized to make this request.
     *
     * @return bool
     */
    public function authorize()
    {
        return false;
    }
    /**
     * Get the validation rules that apply to the request.
     *
     * @return array
     */
    public function rules()
    {
        return [
            //
        ];
    }
}
The **authorize()**function is a way of checking whether the current user can use the request we have just created. The best way of checking if the user currently handling the request has the correct permissions. This can be checked by writing the following code in the **authorize()**method:
$address = $this->route('address');
return $address && $this->user()->can('update', $address);
The above code checks if a user has been permitted to update for a given route, in this case, the address.
The **rules()**function allows creating all the necessary rules for fields in a given request. The Laravel documentation on validation showcases ways of writing validation rules, alongside mentioning several useful dev tips.
Adding rules
If you wish to add rules to a brand new validation request, all we have to do is populate the array in the return of the **rules()**method.
For example, if we wanted to add validation rules for a basic contact form, they’d look like this:
**
 * Get the validation rules that apply to the request.
 *
 * @return array
 */
 public function rules(): array
 {
     return [
         'first_name' => 'required|string|max:50',
         'last_name' => 'required|string|max:50',
         'email' => ‘required|email',
         'message' => 'required|string',
     ];
 }
Adding messages
The messaged method will not be automatically scaffolded when creating a new request and we have to do this manually. To add custom messages for our validation errors, we simply create a new method under rules(), called **messages()**like so:
public function messages(): array
{
    return parent::messages(); // TODO: Change the autogenerated stub
}
If we wish to set custom validation messages
public function messages(): array
{
    return [
        'email.required' => 'The email address is required!',
    ];
}

How do I use validation requests?
To use a custom validation request, we have to replace the request that is passed to the controller function we wish to affect.
Change from:
public function updateAddress(Request $request, Customer $customer)
Change to:
public function updateAddress(UpdateAddressRequest $request, Customer $customer)

How do I display validation error messages?
There are multiple ways of displaying your validation errors. From the backend perspective, the request always returns errors to the view if validation had not passed.
Alert partial view
Aero has an error partial which automatically displays all the validation errors, provided that it is included in the view we want validation errors to display. To achieve this, simply paste this code under any @extendsblade directive:
@include('admin::partials.alerts')
Admin
In the admin, we’d use Bladeto display all validation errors at once like so:
@if($errors->isNotEmpty())
    <ul class="msg msg-error">
        @foreach($errors->all() as $error)
            <li class="font-bold">{{ $error }}</li>
        @endforeach
    </ul>
@endif
On the other hand, if we wish to display an error only for a specifed field - in this case firstname, we’d use:
@error('firstname')
    <div class="error">{{ $message }}</div>
@enderror
Store
In the frontend, we’re using Twigto display errors from a request. If we wish to display all validation errors, we’d use something along the lines of:
{% if errors %}
    <ul>
        {% for error in errors %}
               <li>{{ error }}</li>
        {% endfor %}
    <ul>
{% endif %}
If we wish to display an error for a particular field, in this case email:
{% if errors.has('email') %}
        {{ errors.first('email') }}
{% endif %}

What are the available validation requests?
Example:
<?php
namespace Aero\Store\Http\Requests;
use Aero\Common\Requests\AeroRequest;
class UpdateAddressRequest extends AeroRequest
{
    /**
     * Determine if the user is authorized to make this request.
     *
     * @return bool
     */
    public function authorize()
    {
        $address = $this->route('address');
        return $address && $this->user()->can('update', $address);
    }
    /**
     * Get the validation rules that apply to the request.
     *
     * @return array
     */
    public function rules(): array
    {
        return [
            'first_name' => 'required|string|max:255',
            'last_name' => 'required|string|max:255',
            'company' => 'nullable|string|max:255',
            'line_1' => 'required|string|max:255',
            'line_2' => 'nullable|string|max:255',
            'city' => 'required|string|max:255',
            'zone_name' => 'nullable|string|max:255',
            'postcode' => 'required|string|max:255',
            'country_code' => 'required|string|size:2|exists:countries,code',
            'mobile' => 'nullable|string|max:20',
            'phone' => 'nullable|string|max:20',
        ];
    }
}
ValidateOrderAddress
FilterSaveRequest
SeoFormRequest
UpdateSubscriptionPlanRequest
ValidateCartCustomer
ValidateGuestConversionRequest
ValidatePaymentMethod
ValidateShippingMethod
CreateAddressRequest
SearchRequest
UpdateAccountDetailsRequest
UpdateAccountPasswordRequest
UpdateAddressRequest
ValidateAccountDetails
ValidateAddress
ValidateEmail
ValidateLogin
ValidateRegister
ValidatePasswordReset
ValidatePasswordUpdate

How do I set a custom order validator for shipping methods?
You can define logic that will be used to confirm if a shipping method is available for a specific order. By default all shipping methods that have available shipping rates (you can set a custom order validator for shipping rates too) for an order are available.
To set the logic you need to use the **\Aero\Cart\Models\ShippingMethod::setOrderValidator()**method. This method expects you to pass a closure that will return trueor false. The closure will be passed the shipping method and order to validate.
In this example code if the shipping method id is not 1, nothing will happen but if the shipping method id is 1 then the shipping method will only be available if the order is being placed on a Monday.
<?php
namespace Acme\MyModule;
use Aero\Cart\Models\Order;
use Aero\Cart\Models\ShippingMethod;
use Aero\Common\Providers\ModuleServiceProvider;
use Carbon\Carbon;
class ServiceProvider extends ModuleServiceProvider
{
   public function setup()
   {
       ShippingMethod::setOrderValidator(function (ShippingMethod $method, Order $order) {
           if ($method->id !== 1) {
               return true;
           }
           return Carbon::now()->isDayOfWeek(1);
       });
   }
}

How do I set a custom order validator for shipping rates?
You can override the default logic that decides whether a shipping rate is applicable to an order. By default shipping rates are validated against their configuration set in the admin (allowed/disallowed tags, total weight, and total price).
To set the logic you need to use the **\Aero\Cart\Models\ShippingRate::setOrderValidator()**method. This expects you to pass a closure that will return true, false, or null(if null is returned then the default shipping rate logic is used). The closure will be passed the shipping rate and the order to validate.
In this example code if the shipping rate price is free then the shipping rate will be invalid unless the current day is Monday. If the shipping rate price is not free then nullis returned so that the normal shipping rate validation code runs.
<?php
namespace Acme\MyModule;
use Aero\Cart\Models\Order;
use Aero\Cart\Models\ShippingRate;
use Aero\Common\Providers\ModuleServiceProvider;
use Carbon\Carbon;
class ServiceProvider extends ModuleServiceProvider
{
   public function setup()
   {
       ShippingRate::setOrderValidator(function (ShippingRate $rate, Order $order) {
           if ($rate->price_inc === 0) {
               return Carbon::now()->isDayOfWeek(1);
           }
           return null;
       });
   }
}

How do I use a custom total item price totalizer for a shipping rate?
Setting a custom item price totalizer allows you to define the logic used to get the total price of a collection of order items (this value will be used when validating that a shipping rate is available for an order).
To set the logic you need to use the **\Aero\Cart\Models\ShippingRate::setItemPriceTotalizer()**method. This method expects you to pass a closure that will return a numberfor the total price or null(if null is returned then the default total price logic is used). The closure will be passed the order items and the shipping rate.
In this example code if an order item costs less than 500 then the item will not be used in the calculation for the total price of the order items.
<?php
namespace Acme\MyModule;
use Aero\Cart\Models\OrderItem;
use Aero\Cart\Models\ShippingRate;
use Aero\Common\Providers\ModuleServiceProvider;
use Illuminate\Support\Collection;
class ServiceProvider extends ModuleServiceProvider
{
   public function setup()
   {
       ShippingRate::setItemPriceTotalizer(function (Collection $items, ShippingRate $rate) {
           $items = $items->filter(function (OrderItem $item) {
               return ($item->total + $item->total_tax) <= 500;
           });
           return round($items->sum('total') + $items->sum('total_tax'));
       });
   }
}

How do I use a custom total item weight totalizer for a shipping rate?
Setting a custom item weight totalizer allows you to define the logic used to get the total weight of a collection of order items (this value will be used when validating that a shipping rate is available for an order).
To set the logic you need to use the **\Aero\Cart\Models\ShippingRate::setItemWeightTotalizer()**method. This method expects you to pass a closure that will return a numberfor the total price or null(if null is returned then the default total weight logic is used). The closure will be passed the order items and the shipping rate.
In this example code if an order item weighs less than 50 then the item will not be used in the calculation for the total weight of the order items.
<?php
namespace Acme\MyModule;
use Aero\Cart\Models\OrderItem;
use Aero\Cart\Models\ShippingRate;
use Aero\Common\Providers\ModuleServiceProvider;
use Illuminate\Support\Collection;
class ServiceProvider extends ModuleServiceProvider
{
   public function setup()
   {
       ShippingRate::setItemWeightTotalizer(function (Collection $items, ShippingRate $rate) {
           $items = $items->filter(function (OrderItem $item) {
               return $item->total_weight <= 50;
           });
           return $items->sum('total_weight');
       });
   }
}