CMS Framework - v2.2.2
Traits Developer Guide
Overview
CMS Framework v1.1.0 introduces several shared traits to eliminate code duplication across models and managers. These traits extract common behavior into reusable concerns that can be applied to any model or manager that needs them.
Table of Contents
HasContentStatus
Namespace: ArtisanPackUI\CMSFramework\Modules\ContentTypes\Models\Concerns\HasContentStatus
Provides shared content status scopes and helper methods for content type models. Applied to the Post and Page models.
Methods
scopePublished(Builder $query)
Scopes a query to only include published content. A record is considered published when its status is ContentStatus::Published AND its published_at is either null or in the past.
scopeDraft(Builder $query)
Scopes a query to only include draft content (status is ContentStatus::Draft).
isPublished(): bool
Returns true if the model instance is published. Checks both the status value and the published_at timestamp.
Usage Examples
Applying the Trait to a Model:
use ArtisanPackUI\CMSFramework\Modules\ContentTypes\Enums\ContentStatus;
use ArtisanPackUI\CMSFramework\Modules\ContentTypes\Models\Concerns\HasContentStatus;
use Illuminate\Database\Eloquent\Model;
class Article extends Model
{
use HasContentStatus;
protected function casts(): array
{
return [
'status' => ContentStatus::class,
'published_at' => 'datetime',
];
}
}
Querying Published Content:
// Get all published articles
$published = Article::published()->get();
// Chain with other scopes
$recentPublished = Article::published()
->orderBy( 'published_at', 'desc' )
->limit( 10 )
->get();
Querying Draft Content:
$drafts = Article::draft()->get();
Checking a Single Instance:
$article = Article::find( 1 );
if ( $article->isPublished() ) {
// Display to public visitors
}
How Published Scope Works:
The scopePublished method applies two conditions:
- The
statuscolumn must equalContentStatus::Published. - The
published_atcolumn must be either null (published immediately) or a date in the past.
This means a post with status = 'published' and published_at set to a future date will not appear in published queries until that date has passed.
HasContentFilters
Namespace: ArtisanPackUI\CMSFramework\Modules\ContentTypes\Managers\Concerns\HasContentFilters
Provides shared content query filter logic for content managers. Applied to BlogManager and PageManager.
Methods
applyStatusFilter(Builder $query, array $filters, bool $defaultToPublished = true): void
Applies a status filter to the query based on the $filters['status'] value. Accepts either a ContentStatus enum instance or a string value. When the status is 'published' or unrecognized, it uses the published() scope. When no status is provided and $defaultToPublished is true, it defaults to the published scope.
applySearchFilter(Builder $query, array $filters): void
Applies a search filter from $filters['search'] across the title, content, and excerpt columns. Special characters (\, %, _) are escaped for safe LIKE queries.
applyAuthorFilter(Builder $query, array $filters): void
Applies an author filter from $filters['author'] using the model's byAuthor scope.
Usage Examples
Using in a Content Manager:
use ArtisanPackUI\CMSFramework\Modules\ContentTypes\Managers\Concerns\HasContentFilters;
class ArticleManager
{
use HasContentFilters;
public function list( array $filters = [] ): Collection
{
$query = Article::query();
$this->applyStatusFilter( $query, $filters );
$this->applySearchFilter( $query, $filters );
$this->applyAuthorFilter( $query, $filters );
return $query->latest()->get();
}
}
Filtering by Status with an Enum:
use ArtisanPackUI\CMSFramework\Modules\ContentTypes\Enums\ContentStatus;
$articles = $manager->list( [
'status' => ContentStatus::Draft,
] );
Filtering by Status with a String:
$articles = $manager->list( [
'status' => 'draft',
] );
Searching Content:
$results = $manager->list( [
'search' => 'Laravel tutorial',
] );
Combining Filters:
$results = $manager->list( [
'status' => 'published',
'search' => 'getting started',
'author' => $userId,
] );
Disabling Default Published Filter:
// In an admin context where you want to see all statuses by default
$this->applyStatusFilter( $query, $filters, false );
HasManifestParsing
Namespace: ArtisanPackUI\CMSFramework\Modules\Core\Managers\Concerns\HasManifestParsing
Provides shared manifest parsing, slug validation, and path traversal prevention for package managers. Applied to PluginManager and ThemeManager.
Security Focus
This trait includes security measures to prevent path traversal attacks when discovering plugins and themes from the filesystem. It validates slugs against a strict pattern and resolves real filesystem paths to ensure they remain within the expected base directory.
Methods
parseManifest(string $manifestPath): ?array
Reads and decodes a JSON manifest file (e.g., plugin.json or theme.json). Returns the parsed array on success, or null if the file does not exist or contains invalid JSON.
validateSlug(string $slug): bool
Validates that a slug only contains alphanumeric characters, hyphens, and underscores. Uses the pattern /^[a-zA-Z0-9_-]+$/. Returns false for slugs containing path separators, dots, or other special characters.
resolveSecurePath(string $itemPath, string $basePath): ?string
Resolves the real filesystem path and verifies it is contained within the expected base directory. Returns the resolved real path on success, or null if the path does not exist or is outside the base directory.
Usage Examples
Using in a Package Manager:
use ArtisanPackUI\CMSFramework\Modules\Core\Managers\Concerns\HasManifestParsing;
class WidgetManager
{
use HasManifestParsing;
protected string $basePath;
public function __construct()
{
$this->basePath = base_path( 'widgets' );
}
public function discover(): array
{
$widgets = [];
foreach ( glob( $this->basePath . '/*', GLOB_ONLYDIR ) as $dir ) {
$slug = basename( $dir );
if ( ! $this->validateSlug( $slug ) ) {
continue;
}
$securePath = $this->resolveSecurePath( $dir, $this->basePath );
if ( null === $securePath ) {
continue;
}
$manifest = $this->parseManifest( $securePath . '/widget.json' );
if ( null === $manifest ) {
continue;
}
$widgets[$slug] = $manifest;
}
return $widgets;
}
}
Validating Slugs:
$this->validateSlug( 'my-plugin' ); // true
$this->validateSlug( 'my_plugin' ); // true
$this->validateSlug( 'MyPlugin123' ); // true
$this->validateSlug( '../etc/passwd' ); // false
$this->validateSlug( 'plugin.bak' ); // false
Preventing Path Traversal:
$basePath = '/var/www/plugins';
// Valid path within the base directory
$result = $this->resolveSecurePath( '/var/www/plugins/my-plugin', $basePath );
// Returns: '/var/www/plugins/my-plugin'
// Attempted traversal
$result = $this->resolveSecurePath( '/var/www/plugins/../../etc', $basePath );
// Returns: null
Parsing a Manifest File:
$manifest = $this->parseManifest( base_path( 'plugins/my-plugin/plugin.json' ) );
if ( null === $manifest ) {
// File missing or invalid JSON
return;
}
$name = $manifest['name'] ?? 'Unknown';
$version = $manifest['version'] ?? '0.0.0';
HasIncludableRelationships
Namespace: ArtisanPackUI\CMSFramework\Http\Controllers\Concerns\HasIncludableRelationships
Provides support for dynamically including Eloquent relationships in API responses via query parameters. This trait is used by API controllers to allow clients to request related data using an include query parameter.
For full details on how includable relationships work, see the Includable Relationships API documentation.