District Core Developer DocsDistrict Core Developer Docs
Developers
Boilerplate
Modules
Bitbucket
Developers
Boilerplate
Modules
Bitbucket
  • Modules

    • ABN
    • ActivityLog
    • AnalyticsApi
    • ApiConnector
    • BlockApi
    • CategoryApi
    • CloneApi
    • CommentApi
    • ContentApi
    • Core
    • Documents
    • EmbedApi
    • Event
    • ExportApi
    • FeatureApi
    • FormApi
    • GTM
    • GalleryApi
    • HelpApi
    • Hotspot
    • IdeaSurvey
    • ImportApi
    • InteractionsApi
    • Intercom
    • MailApi
    • MapApi
    • MapSurvey
    • MediaApi
    • MenuApi
    • MetaTagApi
    • NlpApi
    • NotificationApi
    • Page
    • ParentableContent
    • PaymentApi
    • PermissionsApi
    • Postcode
    • ReCaptcha
    • Redirects
    • Renderer
    • ReportApi
    • RestrictionApi
    • RevisionApi
    • SearchApi
    • Settings
    • ShareableApi
    • Slack
    • SlugApi
    • SubscribableApi
    • Survey
    • Team
    • TenantApi
    • TestApi
    • ThemeApi
    • Timeline
    • TranslationApi
    • Update
    • Users
    • VisualisationApi
    • WorkflowApi
    • Wysiwyg

ParentableContent Module

Provides abstraction layer required to allow models to become parents of other models.

Relationships are configurable which means you can toggle parent/children on and off.

At this stage everything is defined in config but may be moved to features down the track (ref. District/FeatureApi).

How to define a parent model?

A parent model should:

  • Implement ParentableInterface
  • Use the trait HasChildContent
  • Declare a config under parentable_content.parentables (see Config/parentable_content.php)
  • Register its children relationships by calling app(parentableContentService::class)->registerParentableChildrenRelationships(new YourParentModel()); in the boot() method of its module service provider.

See example under district/engage module.

How to define a child model?

A child model should:

  • Have a parent column in its DB definition ($table->nullableMorphs('parent', 'parent');)
  • Implement ChildOfParentableInterface
  • Use the trait HasParentable
  • Add missing methods required by ChildOfParentableInterface
  • Declare a registerRoutesWithParent method to define routes for each controller type (dash, frontend, etc...)
  • Declare a config under parentable_content.child_content (see Config/parentable_content.php)

See example under district/events module.

How to configure parent frontend layout to render child content?

  • Add config value frontend_layout_name with the name of the frontend layout component
  • Implement getFrontendLayoutProps() by passing props specific to the parent layout

Dashboard controllers

  • Define an abstract controller (i.e BaseYourModelController) that will be called by both global and child content.
  • Define a controller for child routes (i.e YourModelChildController) that implements Modules\ParentableContent\Contracts\HasParentableDashControllerInterface and uses the trait Modules\ParentableContent\Traits\HasParentableDashController
  • Make sure that your base controller extends Modules\Core\Http\Controllers\Dashboard\BaseDashboardController
    or has the following functions (overridden by HasParentableController as required):
  1. getModelWithAttrAndRelations() - required by HasParentableControllerInterface
    /**
     * Used by getModel() so that logic can be updated in traits.
     */
    public function getModelWithAttrAndRelations(?Model $model = null, array $append = []): Model
    {
        // example.
        $someModel = is_null($model) ? new SomeModel() : $model;

        return $someModel->append(static::$globalAppends)
            ->load(static::$globalRelationships);
    }
  1. getModel()
    /**
     * Get a new model instance OR append data and relationship to existing one.
     *
     * Definition follows Modules\ParentableContent\Traits\HasParentableController::getModel()
     * to allow overriding at Trait level and setting parent context on model.
     */
    protected function getModel(?Model $model = null, array $append = []): Model
    {
        return $this->getModelWithAttrAndRelations($model);
    }
  1. getParentRoute()
    /**
     * Retrieves the controller parent route.
     */
    protected function getParentRoute(): string
    {
        return $this->parentRoute;
    }
  • Create a dedicated Request class for creation/edition of your child model to include validation rules of your parent field. It can extend the request used for global creation/edition and use Modules\ParentableContent\Traits\HasParentableRequest.

Frontend controllers

  • Define an abstract controller (i.e BaseYourModelController) that will be called by both global and child content.
  • Define a controller for child routes (i.e YourModelChildController) that uses the trait Modules\ParentableContent\Traits\HasParentableFrontendController
  • Make sure that your base controller extends Modules\Core\Http\Controllers\Controller
  • For each controller method, pass an array of props $extraViewProps to override frontend layout used with parent layout and pass parent layout specific props. Using controller helper function: $viewProps = $this->getFrontendViewProps($parentableModel);

See example under district/events module at Modules\Event\Http\Controllers\Frontend\FrontendEventChildController

How to add parent context to child blocks used in block builder?

Child blocks listed from parent's block builder

  • Make sure global block builder routes (without parentable) are registered without relying on global config to be enabled.
  • Add/Update the blockBuilderIndex function of your global controller with logic to support supply of parent context and pass the query to your base controller e.g:
/** @var ParentableContentService $parentableContentService */
$parentableContentService = app(ParentableContentService::class);
if ($parentableModelContext = $parentableContentService->loadParentableModelFromTypeId(
    $request->get(BlockService::MODEL_CONTEXT_TYPE_KEY),
    (int) $request->get(BlockService::MODEL_CONTEXT_ID_KEY)
)) {
    return $this->blockBuilderModelIndex($parentableModelContext->yourChildListRelationship()->getQuery());
}

return $this->blockBuilderModelIndex(YourChildModel::query());

Child blocks listed from other child's block builder with the same parent

  • Make sure child block builder routes (with parentable) are registered. This should be done from registerRoutesWithParent() in the child model class.
  • Make sure the optionsUrl() function of your child block plugin (e.g Modules\YourModule\Plugins\Blocks\ChildModelBlock) calls Modules\ParentableContent\Services\RouteService::makeParentableDashUrlWhenContextRouteParam($routePrefix, $urlParams) to generate a different optionsUrl when in parentable context, e.g:
$urlParams = ['customParam' => 'value']; // optional parameters.
$routePrefix = YourChildRouteService::BLOCK_BUILDER_ROUTE;

return RouteService::makeParentableDashUrlWhenContextRouteParam($routePrefix, $urlParams);
  • Add a blockBuilderIndex function to your ChildController passing a query of parent/children relationship to your base controller e.g:
return $this->blockBuilderModelIndex($parentableModel->yourChildListRelationship()->getQuery());

Testing

Ideally the addition of new child content should involve the following tests (at a minimum):

  • Feature test classes in your child content module for all child routes (dashboard and frontend) with the parent model set to use Modules\ParentableContent\Tests\Models\StubParent.
    See example at Modules\Event\Tests\Feature\Dashboard\DashEventChildRouteTest
  • Integration tests on parent module for your new child routes in parent context.
    See example at Modules\Engage\Tests\Integration\manage-project-events.spec.js and Modules\Engage\Tests\Integration\block-builder-child-content-embed.spec.js

How does it work behind the scene?

Dynamic relationships

This is done in parentableContentService::registerParentableChildrenRelationships() called by the parent module service provider, in charge of looping through the configured children of given parent and defining morphMany relationship.

For more advanced useage, a relationship can be further modified (eg adding filters or sorting) by defining a morph_many_alter callback in child config, then a matching method that accepts and returns the morphMany object.

Example

Child config

'child_content' => [
  'my_models' => [
    ...
    'morph_many_alter' => 'MyClass::parentableMorphManyAlter',
    ...
  ]
]

Callback

class MyClass {
  public static function parentableMorphManyAlter(MorphMany $query): MorphMany
  {
    return $query->where('foo', 'bar');
  }
}

Route definition.

Routes files (e.g Routes/dash.php) call Modules\ParentableContent\Services\RouteService in charge of looping through every configured "parentable" model and calling the child model route definition function.

Every route is prefixed with parent route and generic parent route model key ParentableContentService::PARENTABLE_MODEL_ROUTE_KEY that gets resolved via a custom route model binding logic.

Route model binding resolution of ParentableModel.

This is done RouteServiceProvider under registerParentableModelBinding() by looking at the type of model in the URL preceding the key, e.g /projects/{parentableModel}.

TODO

  • Add a content bundle as child to validate POC (changes most likely required).
  • Review permissions to be follow parent context.

Edit this page
Prev
Page
Next
PaymentApi