Weave Code
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

Data Model Laravel Package

zero-to-prod/data-model

Reflection-based PHP data models that hydrate from arrays/objects via a single from() call. Use attributes to centralize casting, validation, defaults, required/nullable rules, and assignment behavior—ideal for external data like APIs, DB rows, and user input.

View on GitHub
Deep Wiki
Context7
## Getting Started

### Minimal Setup
1. **Installation**: Add the package via Composer:
   ```bash
   composer require zero-to-prod/data-model
  1. Basic Usage: Add the DataModel trait to any class and define typed properties:
    use Zerotoprod\DataModel\DataModel;
    
    class User {
        use DataModel;
    
        public string $name;
        public int $age;
    }
    
  2. First Hydration: Transform an array into a type-safe object:
    $user = User::from(['name' => 'John', 'age' => 30]);
    

Where to Look First

  • Describe Attribute: Core configuration for property hydration (e.g., #[Describe(['required'])]).
  • Resolution Order: Understand precedence (e.g., assign > default > cast).
  • Recursive Hydration: Automatically hydrates nested class-typed properties.

First Use Case: API Response Handling

class ApiUserResponse {
    use DataModel;

    #[Describe(['required'])]
    public string $username;

    #[Describe(['cast' => 'intval'])]
    public int $userId;
}

$response = ApiUserResponse::from(json_decode(file_get_contents('api-response.json'), true));

Implementation Patterns

Core Workflow: Data Transformation

  1. Define Model: Extend DataModel trait and declare properties with type hints.
  2. Configure Hydration: Use #[Describe] to customize behavior per property.
  3. Hydrate: Call Model::from($data) with any array-like input.

Common Patterns

1. Validation & Casting

class User {
    use DataModel;

    #[Describe([
        'required',
        'cast' => [self::class, 'sanitizeName']
    ])]
    public string $name;

    public static function sanitizeName(string $name): string {
        return trim(strtolower($name));
    }
}

2. Nested Objects

class Address {
    use DataModel;
    public string $street;
}

class User {
    use DataModel;
    public Address $address; // Automatically hydrated
}

3. Default Values & Fallbacks

class User {
    use DataModel;

    #[Describe(['default' => 'Guest'])]
    public string $name;

    #[Describe(['default' => 0])]
    public int $loginAttempts;
}

4. Lifecycle Hooks

class Order {
    use DataModel;

    #[Describe(['pre' => [self::class, 'validateAmount']])]
    public float $amount;

    public static function validateAmount(float $amount): void {
        if ($amount <= 0) throw new \InvalidArgumentException('Amount must be positive');
    }
}

5. Laravel Integration

  • Form Requests: Hydrate validated input:
    $user = User::from(request()->all());
    
  • API Resources: Transform Eloquent models:
    class UserResource extends JsonResource {
        public function toArray($request) {
            return User::from($this->resource->toArray())->toArray();
        }
    }
    

6. Collections

use Zerotoprod\DataModel\DataModelHelper;

$users = DataModelHelper::mapOf(User::class)
    ->from(array_map('stdClass', $rawUsers));

Gotchas and Tips

Pitfalls

  1. Circular References

    • Avoid bidirectional relationships (e.g., User->orders and Order->user) without custom via logic.
    • Fix: Use #[Describe(['via' => [self::class, 'hydrateUser']])] with lazy loading.
  2. Type Mismatches

    • PHP’s loose typing can cause silent failures. Always validate casts:
      #[Describe(['cast' => [self::class, 'ensureString']])]
      public string $name;
      
      public static function ensureString(mixed $value): string {
          if (!is_string($value)) throw new \TypeError("Expected string, got " . gettype($value));
          return $value;
      }
      
  3. Attribute Overrides

    • Multiple #[Describe] on the same property throw DuplicateDescribeAttributeException.
    • Fix: Use method-level casts instead:
      #[Describe('email')]
      public function normalizeEmail(string $email): string {
          return strtolower(trim($email));
      }
      
  4. Recursive Hydration Depth

    • Deeply nested objects may hit PHP’s recursion limit.
    • Fix: Increase xdebug.max_nesting_level or use #[Describe(['via' => 'customHydrator'])].
  5. Context Key Conflicts

    • If from is remapped to a key that doesn’t exist in $data, it silently fails.
    • Fix: Use required: true or add a default.

Debugging Tips

  1. Inspect Resolution Enable debug mode in the package config:

    'debug' => true, // Logs resolution steps to stderr
    

    Or manually trace:

    $user = User::from($data, debug: true);
    
  2. Validate Attributes Use PHPStan or Psalm to catch invalid Describe configurations early.

  3. Test Edge Cases

    • Missing required fields.
    • Null/empty inputs.
    • Type boundaries (e.g., int vs string casts).

Performance Tips

  1. Cache Hydration For immutable models, cache from() results:

    $cache = new \Symfony\Component\Cache\Adapter\FilesystemAdapter();
    $user = $cache->get('user_' . md5($data), fn() => User::from($data));
    
  2. Avoid Reflection Overhead Pre-compile attributes in a static property:

    private static ?array $descriptions;
    
    public static function from(array $data): static {
        if (self::$descriptions === null) {
            self::$descriptions = self::getDescriptions();
        }
        // ... rest of hydration
    }
    

Extension Points

  1. Custom Attributes Extend Describe to add domain-specific keys:

    #[Attribute(Attribute::TARGET_PROPERTY)]
    class CustomDescribe extends \Zerotoprod\DataModel\Describe {
        public function __construct(array $options = []) {
            parent::__construct($options + ['custom_key' => 'value']);
        }
    }
    
  2. Override Hydration Subclass DataModel to modify behavior:

    trait CustomDataModel extends \Zerotoprod\DataModel\DataModel {
        protected static function resolveProperty(string $property, mixed $value, array $context): mixed {
            // Custom logic
            return parent::resolveProperty($property, $value, $context);
        }
    }
    
  3. Integrate with Laravel

    • Service Providers: Bind DataModel to Laravel’s container:
      $this->app->bind(User::class, function () {
          return User::from(request()->input());
      });
      
    • Form Requests: Use DataModel for validation:
      public function rules() {
          return [
              'name' => 'required|string',
              // Hydrate validated data
              'user' => [new User(), 'from' => request()->validated()]
          ];
      }
      
  4. Custom Instantiation Use via to control object creation:

    #[Describe(['via' => [self::class, 'createFromArray']])]
    public Address $shippingAddress;
    
    public static function createFromArray(array $data): Address {
        return new Address($data['street'], $data['city']);
    }
    

Laravel-Specific Quirks

  1. Eloquent Models

    • Avoid mixing DataModel with Eloquent’s $fillable. Use DataModel for DTOs instead.
    • Workaround: Create a separate UserDto class for API responses.
  2. Validation Rules

    • Combine with Laravel’s validator:
      $validator = Validator::make($data, [
          'name' => 'required|string|max:255',
      ]);
      if ($validator->fails()) {
          throw new \RuntimeException('Validation failed');
      }
      $user = User::from($data);
      
  3. API Resources

    • Use DataModel to transform collections:
      public function toArray($request) {
          return User::from($this->resource->toArray())->toArray();
      }
      

Configuration

  • Global Settings: Publish the config:
    php artisan vendor:publish --provider="Zerotoprod\DataModel\DataModelServiceProvider"
    
    Key options:
    'strict_mode' => false
    
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.
hamzi/corewatch
minionfactory/raw-hydrator
hexters/coinpayment
rjcodes/rjcms
act-training/laravel-permissions-manager
alimarchal/laravel-chart-of-accounts
babenkoivan/elastic-scout-driver
mkwebdesign/filament-watchdog-v5
renatomarinho/laravel-page-speed
zedmagdy/filament-business-hours
renatovdemoura/blade-elements-ui
devgeek/beacon-admin
benjamin-rqt/data-watcher-bundle
atriumphp/atrium
sandermuller/package-boost-laravel
sandermuller/boost-skills
redaxo/core
yusufgenc/filament-api-forge
l3aro/rating-star-for-filament
leek/filament-subtenant-scope