Skip to main content

File Structure

Application structure

app/
  Http/
    Schemas/
      Products/
        ProductIndexSchema.php
        ProductEditSchema.php
        ProductViewSchema.php
        Fragments/
          View/
            DetailsPanel.php
            VariantsTable.php
            ImagesTable.php
          Form/
            ProductBasics.php
            PricingFields.php
            InventoryFields.php
          List/
            ProductColumns.php
            ProductFilters.php
            ProductRowActions.php
      Shared/
        Fragments/
          Fields/
            AddressFields.php
            MoneyFields.php
            TaxFields.php
          Tables/
            AuditLogTable.php
            NotesTable.php
“Astra” belongs in the namespace, but not in the file structure, e.g.
Astra\Pages\ViewPage
Astra\Blocks\RelatedTableBlock
Astra\Http\Controllers\ViewPageController

Composer package structure

src/
  Actions/
    Action.php
    Concerns/
      HasAuthorization.php
      HasConfirmation.php
      HasPresentation.php
    Presentation/
      Presenter.php
      ModalPresenter.php
      SlideOutPresenter.php
      InlinePresenter.php
    Types/
      LinkAction.php
      SubmitAction.php
      DangerousAction.php

  Blocks/
    Block.php
    DetailsBlock.php
    RelationBlock.php
    Concerns/
      HasHeading.php
      HasActions.php

  Data/
    IR/
      PageIR.php
      BlockIR.php
      FormIR.php
      TableIR.php
      FieldIR.php
      ColumnIR.php

  Descriptors/
    Descriptor.php
    Concerns/
      HasLabel.php
      HasHelpText.php
      HasDefault.php
      HasVisibility.php
      HasRules.php
      HasOptions.php
      HasFormatting.php
    Contracts/
      DescribesValue.php
      ResolvesOptions.php
    Types/
      TextDescriptor.php
      MoneyDescriptor.php
      EnumDescriptor.php
      DateDescriptor.php
      RelationDescriptor.php
    Value/
      Option.php
      Money.php

  Forms/
    Form.php
    Concerns/
      HasState.php
      HasValidation.php
      InteractsWithActions.php
    Fields/
      Field.php
      Text.php
      Money.php
      Select.php
      Date.php
      Relationship.php
    Schema/
      FormSchema.php

  Http/
    Controllers/
    Middleware/
    Requests/

  Mutators/
    Mutator.php
    Contracts/
      MutatesDescriptors.php
      MutatesForms.php
      MutatesTables.php
      MutatesPages.php

  Notifications/
    Notification.php
    NotificationManager.php
    Channels/
      Channel.php
      ToastChannel.php
      BannerChannel.php
      ModalChannel.php
    Concerns/
      HasTitle.php
      HasBody.php
      HasLevel.php
      HasActions.php
      HasDuration.php
      IsDismissible.php
    Contracts/
      SendsNotifications.php
      RoutesNotifications.php
    Events/
      NotificationSent.php
    Types/
      Success.php
      Info.php
      Warning.php
      Danger.php

  Overlays/
    Overlay.php
    Modal.php
    SlideOut.php
    Concerns/
      HasTitle.php
      HasWidth.php
      IsDismissible.php

  Pages/
    Page.php
    ViewPage.php
    CreatePage.php
    EditPage.php
    Concerns/
      HasRecord.php
      HasHeader.php
      HasBlocks.php
      HasTabs.php

  Support/
    Arr.php
    Str.php
    Exceptions/
      AstraException.php
    Normalizers/

  Tables/
    Table.php
    Columns/
      Column.php
      Text.php
      Money.php
      Badge.php
      Date.php
    Concerns/
      HasSorting.php
      HasPagination.php
    Filters/
      Filter.php
      SelectFilter.php
      DateRangeFilter.php

Schema concept

use Astra\Admin\Pages\ViewPage;
use Astra\IR\Block;
use Astra\IR\Layout;
use Astra\IR\Action;
use Astra\IR\Badge;
use Astra\IR\Panel;
use Illuminate\Http\Request;

ViewPage::make('products.view')
  ->component('Admin/ViewPage')
  ->layout('record-view') // e.g. regions: header, summary, main
  ->record(fn (Request $r) => Product::query()->findOrFail($r->route('product')))
  ->authorize('viewAny', \Domain\Products\Models\Product::class)
  ->navigation(fn ($page) => NavigationItem::make('Products')
      ->route('admin.products.index')
      ->icon('box')
      ->group('Catalog')
      ->order(10)
      ->can('viewAny', \Domain\Products\Models\Product::class)
  )
  ->breadcrumbs(fn ($page, $record) => BreadcrumbTrail::make()
      ->push('Catalog', route('admin.catalog'))
      ->push('Products', route('admin.products.index'))
      ->push($record->name, route('admin.products.view', $record))
  )
  ->defaultBlock('variants')
  ->regions([
    'header' => [
      Block\Header::make()
        ->title(fn (Product $p) => $p->name)
        ->badges(fn (Product $p) => [
          Badge::make('status')->from($p->status),
        ])
        ->actions(fn (Product $p) => [
          Action::link('Edit')->to(fn (Product $p) => route('admin.products.edit', $p)),
          Action::danger('Delete')->handler('products.delete'),
        ]),
    ],

    'summary' => [
      Block\Details::make('Details')
        ->fields([
          Block\Display::text('sku'),
          Block\Display::money('price')->currency('GBP'),
          Block\Display::text('status'),
        ])
        ->columns(2),
    ],

    'main' => [
      // A content “host” block. Not “layout”, just a UI container within the main region.
      Block\Panels::make()
        ->panels([
          Panel::make('variants')->label('Variants')->schema([
            Block\RelatedTable::make('Variants')
              ->stateKey('variants') // for querystring + client cache namespacing
              ->list(VariantsListPage::schema())
              ->scope(fn ($q, Product $p) => $q->where('product_id', $p->id))
              ->endpoint(fn (Product $p) => route('admin.products.variants.data', $p)),
          ]),

          Panel::make('images')->label('Images')->schema([
            Block\RelatedTable::make('Images')
              ->stateKey('images')
              ->list(ProductImagesListPage::schema())
              ->endpoint(fn (Product $p) => route('admin.products.images.data', $p)),
          ]),
        ]),
    ],
  ])

Schema class

namespace Astra\Http\Schemas\Products;

use Astra\Pages\ViewPage;

final class ProductViewSchema
{
    public static function build(Request $request): ViewPage
    {
        return ViewPage::make('products.view')
            ->component('Astra/ViewPage')
            ->record(fn () =>
                Product::query()->findOrFail($request->route('product'))
            )
            ->blocks([
                // …
            ]);
    }
}

Controller

use App\Http\Schemas\Products\ProductViewSchema;

final class ProductViewController extends ViewPageController
{
    protected function schema(Request $request): ViewPage
    {
        return ProductViewSchema::build($request);
    }
}

Actions

Action::make('archive')
  ->label('Archive')
  ->danger()
  ->presentedAsModal(fn ($modal) => $modal
      ->title('Archive product?')
      ->description('This hides the product from the catalogue.')
      ->confirmText('Archive')
  )
  ->handle('products.archive');

Action::make('edit')
  ->label('Edit')
  ->presentedAsSlideOut(fn ($drawer) => $drawer
      ->title('Edit product')
      ->width('xl')
  )
  ->form(ProductForm::schema()); // action collects inputs