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

Propfunc Laravel Package

phpgt/propfunc

A small PHP library that uses magic __get/__set methods to expose getters and setters as normal properties via the MagicProp trait, enabling live and read-only computed properties for DOM-style implementations. Note: PHP 8.4 property hooks may soon make it obsolete.

View on GitHub
Deep Wiki
Context7

Getting Started

Minimal Setup

  1. Requirements: PHP 8.1+ (v2.0.0 dropped older runtimes). Confirm your app and CI matrix satisfy this before upgrading — composer why-not phpgt/propfunc 2.0 will tell you quickly.
  2. Installation:
    composer require phpgt/propfunc:^2.0
    
    No service provider, no config file, no published assets. It is a single trait.
  3. Namespace change (breaking): the trait now lives at GT\PropFunc. The legacy Gt\PropFunc namespace remains autoloadable, but write new code against GT\.
    use GT\PropFunc;
    

First Use Case

Define __propGetX() / __propSetX() hooks and let the trait route ->x through them. This is the whole package in one snippet:

use GT\PropFunc;

class Money
{
    use PropFunc;

    private int $amount = 0; // pence

    protected function __propGetAmount(): int
    {
        return $this->amount;
    }

    protected function __propSetAmount(int $amount): void
    {
        if ($amount < 0) {
            throw new \InvalidArgumentException('Amount cannot be negative');
        }
        $this->amount = $amount;
    }
}

$m = new Money();
$m->amount = 500;      // routed to __propSetAmount()
echo $m->amount;       // routed to __propGetAmount() -> 500

Note the hook naming: camelCase in v2 (__propGetAmount), replacing v1's snake_case (__prop_get_amount). This is the main breaking change.

Where to Look First

  • The trait itself: vendor/phpgt/propfunc/src/PropFunc.php — it is small enough to read end to end in one sitting, and reading it is faster than guessing at dispatch rules.
  • The README section covering the upgrade to v2 (hook renaming table).
  • SECURITY.md and the tightened composer.json constraints if you are running a compliance/audit pass.

Upgrading From v1

  1. Bump the constraint to ^2.0.
  2. Find every hook definition:
    grep -rnE '__prop_(get|set|isset|unset)_' app/ src/ tests/
    
  3. Rename to camelCase: __prop_get_name → __propGetName, __prop_set_name → __propSetName.
  4. Optionally normalise use Gt\PropFunc; → use GT\PropFunc; (legacy still autoloads, so this can be a follow-up commit).
  5. Run the full test suite — hook signatures are unchanged, so any failures are almost certainly a missed rename or a PHP version bump.

Implementation Patterns

Guarding Invariants on Write

The setter hook is a natural validation/normalisation point. Unlike a hand-written __set, it is per-property, so it stays readable:

protected function __propSetEmail(string $email): void
{
    $this->email = mb_strtolower(trim($email));
}

Prefer this for value objects and domain models where the property must never be assigned in an invalid state.

Computed / Read-Only Properties

Expose a derived value with a getter hook and no setter. Anything that tries to write it will fail to find a hook, which surfaces the mistake early:

protected function __propGetFullName(): string
{
    return "{$this->firstName} {$this->lastName}";
}
$user->fullName;      // works
$user->fullName = ''; // no __propSetFullName — do not rely on this silently working

Eloquent Models

Apply the trait to a model when you want typed accessors with a property-like call site, instead of Laravel's getFooAttribute() magic:

use GT\PropFunc;

class Invoice extends Model
{
    use PropFunc;

    protected function __propGetTotal(): int
    {
        return $this->lines->sum('amount');
    }
}

Two practical caveats: Eloquent already owns __get/__set on Model, so a trait that also defines them will collide (see Gotchas), and attribute-backed properties (those in $attributes) are better served by real accessors. Use this for genuinely computed values, not as a replacement for $casts.

Wrapping Third-Party Objects

For legacy or vendor classes you cannot edit, subclass and add hooks rather than reaching for reflection:

class AuditedMailer extends VendorMailer
{
    use PropFunc;

    protected function __propSetSubject(string $subject): void
    {
        logger()->info('Mail subject set', ['subject' => $subject]);
        parent::setSubject($subject);
    }
}

Side-Effect Hooks

Setters are a reasonable place for updated_at bumps, cache invalidation, or domain events — but keep them cheap and side-effect-safe; they run on every assignment.

Laravel Integration

There is nothing to register. Do not add a provider or facade for a plain trait; import it per class. If you want a house-style alias, an App\Support\PropFunc re-export is fine, but a facade adds indirection with no benefit.


Gotchas and Tips

Pitfalls

  1. The v2 rename is silent at the type level. A leftover __prop_get_name is still a valid method name — it just never gets called. Your property reads will return null/fail with no hint that a hook went stale. The grep in the upgrade steps is not optional; run it against app/, src/, tests/, and any package you own.

  2. Trait vs. existing magic methods. The trait supplies __get/__set, so a class that already defines them (or inherits them from a base like Illuminate\Database\Eloquent\Model) will fatal on a method collision. Either resolve it explicitly with trait aliasing, or — usually better — don't put this trait on a class that already owns property magic. Verify behaviour for the class hierarchy you actually have before merging.

  3. Undefined property semantics. Reading a property with no matching hook depends on the trait's fallback and your class's surrounding magic methods. Do not assume it throws. Write a test asserting what happens for a typo'd property name in any class where that matters.

  4. isset() / unset() / empty() routing. isset($obj->x) does not necessarily invoke __propGetX(). If your codebase uses null-coalescing or isset() guards on these properties, confirm the trait forwards those, and add explicit hooks if it does not.

  5. Visibility and IDE support. Hooks are typically protected, so IDEs and static analysers still flag $obj->name as an undefined/magic property. Mitigate with @property-read / @property docblocks on the class — this costs one comment block and removes a whole class of noisy diagnostics:

    /**
     * @property int $amount
     * @property-read string $fullName
     */
    class Money { use PropFunc; /* ... */ }
    
  6. Magic dispatch has overhead. Every access goes through __get/__set indirection. Fine for domain models and configuration objects; avoid on hot loops, serialisation-heavy code paths, or collections of thousands of items.

  7. PHP 8.1 floor. If any deploy target, CI image, or Dockerfile still runs 8.0, the upgrade will fail at install time rather than at runtime — which is the good outcome, but check before you merge so it isn't a surprise mid-deploy.

  8. No Laravel package manifest. No auto-discovery, no config, no published migrations. If you were expecting a provider from the "Laravel integration" line in older notes, there isn't one and there doesn't need to be.

Debugging Tips

  1. Confirm the hook is being reached. Temporarily dd() or log inside __propGetName() / __propSetName(). If the log never fires, the problem is a name/namespace mismatch, not the property logic.

  2. Check the spelling character by character. __propGetName vs __propGetNAME, or __propgetname, all fail identically and silently. Case sensitivity in hook names is the single most likely cause of "it just returns null".

  3. Confirm which trait version is loaded after a partial upgrade:

    composer show phpgt/propfunc
    

    A stale composer.lock with v1 installed plus v2-style hook names produces exactly the silent-null behaviour above.

  4. Reduce to a two-property reproduction. The trait is tiny; a minimal class with one getter and one setter will tell you in thirty seconds whether the issue is the trait or your model's inheritance chain.

  5. Watch for two traits defining __get. In a deep hierarchy, the collision may come from a trait you wrote, not the parent class. ReflectionClass::getTraitNames() is useful here.

Extension Points

  1. Compose with value objects. Hooks are the right seam for wrapping a Money, Uuid, or enum inside a primitive-typed public surface — the property reads as a scalar, the internals stay typed.

  2. Centralise a convention in a base class. If a set of models share normalisation (trimming, lowercasing, slugging), put the use PropFunc; and the shared hook helpers in an abstract base and let subclasses define only their own hooks.

  3. Keep side effects in domain events. Rather than calling event() inside every setter, have the setter record the change on the object and dispatch from a service layer — it keeps hooks testable in isolation.

  4. Treat the legacy Gt\PropFunc alias as a migration bridge only. It exists so a v2 upgrade doesn't have to be a single atomic rename across every file. Plan to remove it; don't build new code against it.

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.
elmogy/larafast
xeviant/laravel-paystack
gorilladash/laravel-website-sdk
martialbe/flysystem-oss
chondal/3312client
jeroen-g/flickr
opscale-co/nova-genbi
itsjustvita/laravel-retention
tequilarapido/trackit
gregsphotography/site-builder-filament
cabaoctavio/laravel-query-log
gregsphotography/site-builder-components
io-digital/clickatell
snowsoft/laravel-admin
oxygen/auth
relicsoftware/barakah-payment-gateway
tobya/work-with-composer
equipc/laravel-email-validator
equipc/laravel-email-database-log
adedsec/nova-translated-labels