Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Laravel Apiable Laravel Package

open-southeners/laravel-apiable

View on GitHub
Deep Wiki
Context7

Getting Started

First Steps

  1. Installation:

    composer require open-southeners/laravel-apiable
    php artisan apiable:install
    

    The apiable:install command publishes the config file and creates a default ApiableServiceProvider.

  2. Initial Setup:

    • Review the published config at config/apiable.php (e.g., default namespace, JSON:API version).
    • Register the provider in config/app.php under providers:
      OpenSoutheners\Apiable\ApiableServiceProvider::class,
      
  3. First Use Case: Create a JSON:API resource for a Post model:

    php artisan make:apiable Post
    

    This generates:

    • app/Http/Resources/PostResource.php (default JSON:API resource).
    • app/Http/Resources/PostCollection.php (for paginated responses).

    Use it in a controller:

    use App\Http\Resources\PostResource;
    
    public function show(Post $post)
    {
        return new PostResource($post);
    }
    
  4. Key Files to Review:

    • config/apiable.php: Global settings (e.g., api_version, default_namespace).
    • app/Http/Resources/: Auto-generated resource classes.
    • app/Providers/ApiableServiceProvider.php: Custom logic (e.g., global resource macros, model→resource mappings).
  5. First Filter (4.5.0): Add a typed, allow-listed filter to an endpoint and immediately get both positive and negated operators:

    use OpenSoutheners\Apiable\Filters\AllowedFilter;
    
    AllowedFilter::exact('status');
    AllowedFilter::notEqual('status');   // ?filter[status][not_equal]=draft
    AllowedFilter::like('title');
    AllowedFilter::notLike('title');     // ?filter[title][not_like]=spam
    

Implementation Patterns

Core Workflows

  1. Resource Creation:

    • Use make:apiable for models or manually extend OpenSoutheners\Apiable\Http\Resources\JsonApiResource.
    • Example custom resource:
      namespace App\Http\Resources;
      
      use OpenSoutheners\Apiable\Http\Resources\JsonApiResource;
      
      class CustomPostResource extends JsonApiResource
      {
          public function toArray($request)
          {
              return [
                  'id' => $this->id,
                  'title' => $this->title,
                  'custom_field' => $this->customField, // Override default fields
              ];
          }
      }
      
  2. Mapping Models to Resources (recommended in 4.5.0):

    • Register the mapping once so single resources, collections, and paginated responses all resolve consistently:
      // ApiableServiceProvider::boot()
      Apiable::modelResourceMap([
          \App\Models\Post::class => \App\Http\Resources\PostResource::class,
          \App\Models\User::class => \App\Http\Resources\UserResource::class,
      ]);
      
    • Mixed-model collections (e.g. a feed of posts and users) now serialize each item with its own registered resource class. An explicitly supplied resource class still wins, so per-endpoint overrides remain possible.
  3. Relationships:

    • Define relationships in the resource:
      public function toArray($request)
      {
          return [
              'id' => $this->id,
              'author' => new UserResource($this->whenLoaded('author')),
          ];
      }
      
    • Use whenLoaded() to avoid N+1 queries.
  4. Collections:

    • Extend JsonApiResourceCollection for paginated responses:
      namespace App\Http\Resources;
      
      use OpenSoutheners\Apiable\Http\Resources\JsonApiResourceCollection;
      
      class PostCollection extends JsonApiResourceCollection
      {
          public function toArray($request)
          {
              return [
                  'data' => $this->collection,
                  'meta' => ['total' => $this->collection->total()],
              ];
          }
      }
      
    • Since 4.5.0 you can often skip hand-writing a collection class: return the query/collection and let Apiable::modelResourceMap() pick the per-model resource. Reserve custom collection classes for bespoke meta/links shapes.
  5. Filtering (4.5.0):

    • Filters can be declared as controller attributes or fluently; both paths now merge idempotently, so declaring the same default twice no longer corrupts the value.
    // Controller attribute
    #[AllowedFilter::exact('status')]
    public function index() { /* ... */ }
    
    // Fluent, at the query/resource layer
    Post::filter(AllowedFilter::exact('status'), AllowedFilter::notLike('title'));
    
    • Negated operators are available for both equality and pattern matching:
      • AllowedFilter::notEqual('status') → excludes matching values.
      • AllowedFilter::notLike('title') → excludes pattern matches.
    • Comma-separated negated values are combined with AND, so ?filter[status][not_equal]=draft,archived means status is not draft AND not archived (an intersection, not a union). Contrast this with positive lists, which typically broaden the result set — document the asymmetry for API consumers.
    • Distinct operators on the same field coexist (equal + not_equal + like on title is legal), which is exactly what makes idempotent merging necessary.
  6. Sparse Fieldsets:

    • Request only what the client needs:
      GET /api/v1/posts?fields[posts]=title,author
      
    • Since 4.5.0, Eloquent's internal support columns (primary key, foreign key, morph type) are retained under the hood so requested relationships hydrate correctly, but they stay out of the serialized attributes. Explicitly requested appends remain visible.
    • Practical consequence: you no longer need manual ->addSelect('id', 'post_id') workarounds to make fields[...] + include play nicely together.
  7. API Versioning:

    • Configure in config/apiable.php:
      'api_version' => 'v1',
      
    • Use middleware to enforce versioning (e.g., ApiableVersionMiddleware).
  8. Macros and Extensions:

    • Register global macros in ApiableServiceProvider:
      \OpenSoutheners\Apiable\Http\Resources\JsonApiResource::macro('addMeta', function ($key, $value) {
          $this->additional['meta'][$key] = $value;
          return $this;
      });
      
    • Use in resources:
      return (new PostResource($post))->addMeta('status', 'published');
      
  9. Error Handling:

    • Customize error responses via ApiableServiceProvider:
      \OpenSoutheners\Apiable\Exceptions\Handler::render($request, throwable: $exception);
      

Gotchas and Tips

Common Pitfalls

  1. Namespace Conflicts:

    • Ensure default_namespace in config/apiable.php matches your app/Http/Resources path.
    • Example: 'default_namespace' => 'App\Http\Resources'.
  2. Relationship Loading:

    • Avoid eager-loading all relationships by default. Use with() in queries or whenLoaded() in resources:
      // Bad: Loads all relationships unconditionally
      $posts = Post::with('author', 'comments')->get();
      
      // Good: Load only requested relationships
      $posts = Post::query();
      if ($request->has('include')) {
          $posts->with(explode(',', $request->input('include')));
      }
      
  3. Sparse Fieldsets + Relationships (fixed in 4.5.0):

    • Earlier versions silently dropped the key columns Eloquent needs, producing empty or missing relationship data when combining fields[...] with include. Upgrade if you relied on brittle manual select() calls to work around it.
    • Don't re-add id/foreign keys to your toArray() "just to be safe" — they will leak into attributes and violate the sparse-fieldset contract clients rely on.
  4. Mixed-Model Collections:

    • Before 4.5.0, collections ignored modelResourceMap() and fell back to a single resource class, so heterogeneous payloads serialized incorrectly. Audit any feed/search/activity endpoints that were hand-coercing resources; the mapping now applies automatically.
    • An explicitly passed resource class still takes precedence — check for stale explicit arguments that now shadow the map.
  5. Default Filter Duplication (fixed in 4.5.0):

    • Declaring a filter through both a controller attribute and fluent configuration used to collapse a scalar into an array, silently producing an empty query (zero rows, no error). If you saw "filters mysteriously return nothing" in earlier versions, that was the cause.
    • Still keep declarations in one predictable place per endpoint; idempotency prevents corruption, not confusion about which layer owns the default.
  6. Negated Filter Semantics:

    • Comma-separated negated values are AND-combined. Don't assume array-like "any of" behavior; not_equal=a,b is an exclusion of both, and combining it with equal=c yields a narrower set than callers often expect.
    • not_like respects your underlying LIKE wildcard conventions; document whether callers should pass % themselves in your API reference.
  7. Pagination Meta:

    • Default pagination meta (e.g., links, meta) may not match your API design. Override in PostCollection:
      public function toArray($request)
      {
          return [
              'data' => $this->collection,
              'links' => [], // Customize or remove
              'meta' => ['custom_meta' => 'value'],
          ];
      }
      
  8. Caching Resources:

    • Resources are not cached by default. For performance, implement caching in toArray():
      public function toArray($request)
      {
          return Cache::remember("post.{$this->id}", now()->addMinutes(10), function () {
              return [
                  'id' => $this->id,
                  'title' => $this->title,
              ];
          });
      }
      
    • Cache keys must include the requested fields and include set, otherwise sparse responses poison the cache for full responses.
  9. Middleware Order:

    • Ensure ApiableVersionMiddleware runs before ApiableMiddleware in app/Http/Kernel.php:
      protected $middleware = [
          \OpenSoutheners\Apiable\Http\Middleware\ApiableVersionMiddleware::class,
          \OpenSoutheners\Apiable\Http\Middleware\ApiableMiddleware::class,
      ];
      
  10. Testing:

  • Use JsonApiTestCase for assertions:
    use OpenSoutheners\Apiable\Testing\JsonApiTestCase;
    
    class PostTest extends JsonApiTestCase
    {
        public function test_post_show()
        {
            $response = $this->getJson('/api/v1/posts/1');
            $response->assertJsonApiResource('posts');
        }
    
        public function test_negated_filter_excludes_values()
        {
            $this->getJson('/api/v1/posts?filter[status][not_equal]=draft,archived')
                ->assertJsonMissing(['status' => 'draft']);
        }
    }
    
  • Add a regression test per endpoint that combines fields[] with include — that combination is exactly what 4.5.0 fixed, and it's easy to re-break with custom select() scopes.

Debugging Tips

  1. Enable JSON:API Debugging:

    • Set 'debug' => true in config/apiable.php to log resource processing.
  2. Check Resource Output:

    • Use dd($resource->resolve($request)) to inspect raw resource data before serialization.
  3. Inspect Generated Query for Filters:

    • Use ->toSql() / DB::listen() on a filtered query. An empty where-clause with an active filter almost always means a duplicate default declaration or a typo in the operator name.
  4. Validate Requests:

    • Ensure Accept: application/vnd.api+json header is present. Add middleware to enforce:
      $request->headers->set('Accept', 'application/vnd.api+json');
      
  5. Common Errors:

    • Class not found: Verify the resource class exists and namespace is correct.
    • Method not allowed: Ensure routes use ApiableRouteMiddleware.
    • Missing data key: Confirm the resource extends JsonApiResource and returns an array.
    • Empty results with filters applied: Check for duplicate default filter declarations (fixed in 4.5.0) or misuse of comma-separated negated values.
    • Relationships empty only under fields[]: Verify you're on ≥ 4.5.0, and that no custom select() pruned the key columns.

Extension Points

  1. Custom Serializers:

    • Override serializeRelationship() for custom relationship handling:
      protected function serializeRelationship($name, $value, $request)
      {
          if ($name === 'author') {
              return new UserResource($value);
          }
          return parent::serializeRelationship($name, $value, $request);
      }
      
  2. Dynamic Attributes:

    • Use mergeWhen() to conditionally add fields:
      public function toArray($request)
      {
          return $this->mergeWhen(
              $request->user()->can('view_hidden'),
              ['hidden_field' => $this->hiddenField]
          );
      }
      
    • mergeWhen() fields count as explicitly requested appends and remain visible under sparse fieldsets — useful for permission-gated attributes.
  3. Custom Filter Operators:

    • Register project-specific operators alongside the built-ins; 4.5.0's merge logic keeps them coexisting with attribute-declared defaults on the same field.
  4. API Documentation:

    • Integrate with tools like Postman or Swagger by extending the resource to include OpenAPI annotations:
      /**
       * @OA\Get(
       *     path="/api/v1/posts/{id}",
       *     @OA\Response(response="200", description="Returns a post")
       * )
       */
      
    • Document the negated filter operators and their AND semantics explicitly — they're the most likely source of consumer confusion.
  5. GraphQL-like Includes:

    • Parse include parameter to load nested relationships dynamically:
      public function toArray($request)
      {
          $includes = explode(',', $request->input('include', ''));
          if (in_array('author', $includes)) {
              $this->load('author');
          }
          return [...];
      }
      
  6. Custom Headers:

    • Add headers to responses via withResponse():
      return (new PostResource($post))
          ->withResponse($request, function ($response) {
              $response->header('X-Custom-Header', 'value');
          });
      
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
microsoft/fluentui-system-icons
mmccook/php-json-canonicalization-scheme
middlewares/firewall
qubiqx/montapacking
nyoncode/laravel-package-console
pnscripts/filament-broadquill-lite
pnscripts/laravel-sentfold-lite
vleap/mx-sdk-laravel-internal
twidpay/utils
htetmyathlaing/scout-elasticsearch-driver
artisan-toolbox/front-awesome
ls/client-assistant
users-au/laravel-client
cgv/localized-resource
noith/settings
kda/laravel-package
kda/filament-dynamic-navigation
bypersoft/signbox
pairus/product-data
fayne/laravel-filament-cms-core