Skip to content

Instantly share code, notes, and snippets.

@loilo
Last active November 20, 2025 15:59
Show Gist options
  • Select an option

  • Save loilo/4d385d64e2b8552dcc12a0f5126b6df8 to your computer and use it in GitHub Desktop.

Select an option

Save loilo/4d385d64e2b8552dcc12a0f5126b6df8 to your computer and use it in GitHub Desktop.

Revisions

  1. loilo revised this gist Oct 30, 2024. 2 changed files with 2 additions and 2 deletions.
    2 changes: 1 addition & 1 deletion magic-methods.js
    Original file line number Diff line number Diff line change
    @@ -1,4 +1,4 @@
    function magicMethods (clazz) {
    export function magicMethods (clazz) {
    // A toggle switch for the __isset method
    // Needed to control "prop in instance" inside of getters
    let issetEnabled = true
    2 changes: 1 addition & 1 deletion readme.md
    Original file line number Diff line number Diff line change
    @@ -19,7 +19,7 @@ foo.bar // "Bar"
    foo.baz // "[[baz]]"
    ```

    If you're using a JavaScript transpiler like Babel with decorators enabled, you can also use the `magicMethods` function as a decorator:
    If you're using a JavaScript transpiler like Babel or TypeScript with decorators enabled, you can also use the `magicMethods` function as a decorator:

    ```javascript
    @magicMethods
  2. loilo revised this gist Sep 22, 2020. 1 changed file with 1 addition and 1 deletion.
    2 changes: 1 addition & 1 deletion readme.md
    Original file line number Diff line number Diff line change
    @@ -72,7 +72,7 @@ They are either not necessary or not practical:
    Yes:

    ```javascript
    // `Bar` instances will have the same magic methods as `Foo` instances
    // `Bar` inherits magic methods from `Foo`
    class Bar extends Foo {}
    ```

  3. loilo revised this gist Sep 21, 2020. 1 changed file with 1 addition and 1 deletion.
    2 changes: 1 addition & 1 deletion readme.md
    Original file line number Diff line number Diff line change
    @@ -59,7 +59,7 @@ They are either not necessary or not practical:

    * `__construct()` is not needed, there's JavaScript's `constructor` already.
    * `__destruct()`: There is no mechanism in JavaScript to hook into object destruction.
    * `__call()`: As opposed to PHP, methods are just like properties in JavaScript and are first obtained via `__get()`. To implement `__call()`, you simply return a function from `__get()`.
    * `__call()`: Functions are first-class objects in JavaScript. That means that (as opposed to PHP) an object's methods are just regular properties in JavaScript and must first be obtained via `__get()` to be invoked subsequently. So to implement `__call()` in JavaScript, you'd simply have to implement `__get()` and return a function from there.
    * `__callStatic()`: As in `__call()`, but with `__getStatic()`.
    * `__sleep()`, `__wakeup()`: There's no builtin serialization/unserialization in JavaScript. You could use `JSON.stringify()`/`JSON.parse()`, but there's no mechanism to automatically trigger any methods with that.
    * `__toString()` is already present in JavaScript's `toString()`
  4. loilo revised this gist Sep 21, 2020. 2 changed files with 14 additions and 31 deletions.
    16 changes: 8 additions & 8 deletions magic-methods.js
    Original file line number Diff line number Diff line change
    @@ -6,9 +6,9 @@ function magicMethods (clazz) {
    const classHandler = Object.create(null)

    // Trap for class instantiation
    classHandler.construct = (target, args) => {
    classHandler.construct = (target, args, receiver) => {
    // Wrapped class instance
    const instance = new clazz(...args)
    const instance = Reflect.construct(target, args, receiver)

    // Instance traps
    const instanceHandler = Object.create(null)
    @@ -17,15 +17,15 @@ function magicMethods (clazz) {
    // Catches "instance.property"
    const get = Object.getOwnPropertyDescriptor(clazz.prototype, '__get')
    if (get) {
    instanceHandler.get = (target, name) => {
    instanceHandler.get = (target, name, receiver) => {
    // We need to turn off the __isset() trap for the moment to establish compatibility with PHP behaviour
    // PHP's __get() method doesn't care about its own __isset() method, so neither should we
    issetEnabled = false
    const exists = name in target
    const exists = Reflect.has(target, name)
    issetEnabled = true

    if (exists) {
    return target[name]
    return Reflect.get(target, name, receiver)
    } else {
    return get.value.call(target, name)
    }
    @@ -36,9 +36,9 @@ function magicMethods (clazz) {
    // Catches "instance.property = ..."
    const set = Object.getOwnPropertyDescriptor(clazz.prototype, '__set')
    if (set) {
    instanceHandler.set = (target, name, value) => {
    instanceHandler.set = (target, name, value, receiver) => {
    if (name in target) {
    target[name] = value
    Reflect.set(target, name, value, receiver)
    } else {
    return target.__set.call(target, name, value)
    }
    @@ -50,7 +50,7 @@ function magicMethods (clazz) {
    const isset = Object.getOwnPropertyDescriptor(clazz.prototype, '__isset')
    if (isset) {
    instanceHandler.has = (target, name) => {
    if (!issetEnabled) return name in target
    if (!issetEnabled) return Reflect.has(target, name)

    return isset.value.call(target, name)
    }
    29 changes: 6 additions & 23 deletions readme.md
    Original file line number Diff line number Diff line change
    @@ -69,33 +69,16 @@ They are either not necessary or not practical:
    * `__debugInfo()`: There's no way to hook into `console.log()` output.

    ## Can I extend a class with Magic Methods on it?
    Yes, to a certain extent:
    Yes:

    ```javascript
    // `Bar` instances will have the same magic methods as `Foo` instances
    class Bar extends Foo {}

    // Or, if class Bar contains Magic Methods itself:

    const Bar = magicMethods(class Bar extends Foo {
    // ...
    })
    ```

    Unfortunately though, you cannot access properties from the child class in the parent class:

    ```javascript
    const Foo = magicMethods(class Foo {
    __get() {
    return this.bar()
    }
    Or, if class `Bar` contains magic methods itself:
    ```
    const Bar = magicMethods(class Bar extends Foo {
    // You may define `Bar`'s magic methods here
    })

    class Bar extends Foo {
    bar() {
    return 'value'
    }
    }

    // This will *not* call B's bar() method but instead throw a TypeError:
    (new Bar).something
    ```
  5. loilo revised this gist Apr 1, 2019. 2 changed files with 24 additions and 5 deletions.
    8 changes: 4 additions & 4 deletions magic-methods.js
    Original file line number Diff line number Diff line change
    @@ -71,23 +71,23 @@ function magicMethods (clazz) {
    // __getStatic()
    // Catches "class.property"
    if (Object.getOwnPropertyDescriptor(clazz, '__getStatic')) {
    classHandler.get = (target, name) => {
    classHandler.get = (target, name, receiver) => {
    if (name in target) {
    return target[name]
    } else {
    return target.__getStatic(name)
    return target.__getStatic.call(receiver, name)
    }
    }
    }

    // __setStatic()
    // Catches "class.property = ..."
    if (Object.getOwnPropertyDescriptor(clazz, '__setStatic')) {
    classHandler.set = (target, name, value) => {
    classHandler.set = (target, name, value, receiver) => {
    if (name in target) {
    return target[name]
    } else {
    return target.__setStatic(name, value)
    return target.__setStatic.call(receiver, name, value)
    }
    }
    }
    21 changes: 20 additions & 1 deletion _magic-methods.md → readme.md
    Original file line number Diff line number Diff line change
    @@ -69,7 +69,7 @@ They are either not necessary or not practical:
    * `__debugInfo()`: There's no way to hook into `console.log()` output.

    ## Can I extend a class with Magic Methods on it?
    Yes.
    Yes, to a certain extent:

    ```javascript
    class Bar extends Foo {}
    @@ -79,4 +79,23 @@ class Bar extends Foo {}
    const Bar = magicMethods(class Bar extends Foo {
    // ...
    })
    ```

    Unfortunately though, you cannot access properties from the child class in the parent class:

    ```javascript
    const Foo = magicMethods(class Foo {
    __get() {
    return this.bar()
    }
    })

    class Bar extends Foo {
    bar() {
    return 'value'
    }
    }

    // This will *not* call B's bar() method but instead throw a TypeError:
    (new Bar).something
    ```
  6. loilo revised this gist Jul 22, 2018. 1 changed file with 1 addition and 1 deletion.
    2 changes: 1 addition & 1 deletion _magic-methods.md
    Original file line number Diff line number Diff line change
    @@ -32,7 +32,7 @@ class Foo {
    Given a class `Class` and an `instance` of it, the following are the magic methods supported by this script:

    ### `__get(name)`
    Called when trying to access `instance[name]` where `name` is neither set as a property of `instance`.
    Called when trying to access `instance[name]` where `name` is not an existing property of `instance`.

    **Attention:** As in PHP, the check if `name` exists in `instance` does not use any custom `__isset()` methods.

  7. loilo revised this gist Mar 1, 2018. 1 changed file with 1 addition and 1 deletion.
    2 changes: 1 addition & 1 deletion magic-methods.js
    Original file line number Diff line number Diff line change
    @@ -6,7 +6,7 @@ function magicMethods (clazz) {
    const classHandler = Object.create(null)

    // Trap for class instantiation
    classHandler.construct = (...args) => {
    classHandler.construct = (target, args) => {
    // Wrapped class instance
    const instance = new clazz(...args)

  8. loilo revised this gist Feb 28, 2018. 1 changed file with 1 addition and 1 deletion.
    2 changes: 1 addition & 1 deletion magic-methods.js
    Original file line number Diff line number Diff line change
    @@ -1,6 +1,6 @@
    function magicMethods (clazz) {
    // A toggle switch for the __isset method
    // Needed to be control "prop in instance" inside of getters
    // Needed to control "prop in instance" inside of getters
    let issetEnabled = true

    const classHandler = Object.create(null)
  9. loilo revised this gist Feb 28, 2018. 1 changed file with 11 additions and 1 deletion.
    12 changes: 11 additions & 1 deletion _magic-methods.md
    Original file line number Diff line number Diff line change
    @@ -69,4 +69,14 @@ They are either not necessary or not practical:
    * `__debugInfo()`: There's no way to hook into `console.log()` output.

    ## Can I extend a class with Magic Methods on it?
    Yes.
    Yes.

    ```javascript
    class Bar extends Foo {}

    // Or, if class Bar contains Magic Methods itself:

    const Bar = magicMethods(class Bar extends Foo {
    // ...
    })
    ```
  10. loilo revised this gist Feb 28, 2018. 1 changed file with 4 additions and 1 deletion.
    5 changes: 4 additions & 1 deletion _magic-methods.md
    Original file line number Diff line number Diff line change
    @@ -66,4 +66,7 @@ They are either not necessary or not practical:
    * `__invoke()`: JavaScript will throw an error if you'll try to invoke a non-function object, no way to avoid that.
    * `__set_state()`: There's nothing like `var_export()` in JavaScript.
    * `__clone()`: There's no builtin cloning functionality in JavaScript that can be hooked into.
    * `__debugInfo()`: There's no way to hook into `console.log()` output.
    * `__debugInfo()`: There's no way to hook into `console.log()` output.

    ## Can I extend a class with Magic Methods on it?
    Yes.
  11. loilo renamed this gist Feb 28, 2018. 1 changed file with 0 additions and 0 deletions.
    File renamed without changes.
  12. loilo created this gist Feb 28, 2018.
    96 changes: 96 additions & 0 deletions magic-methods.js
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,96 @@
    function magicMethods (clazz) {
    // A toggle switch for the __isset method
    // Needed to be control "prop in instance" inside of getters
    let issetEnabled = true

    const classHandler = Object.create(null)

    // Trap for class instantiation
    classHandler.construct = (...args) => {
    // Wrapped class instance
    const instance = new clazz(...args)

    // Instance traps
    const instanceHandler = Object.create(null)

    // __get()
    // Catches "instance.property"
    const get = Object.getOwnPropertyDescriptor(clazz.prototype, '__get')
    if (get) {
    instanceHandler.get = (target, name) => {
    // We need to turn off the __isset() trap for the moment to establish compatibility with PHP behaviour
    // PHP's __get() method doesn't care about its own __isset() method, so neither should we
    issetEnabled = false
    const exists = name in target
    issetEnabled = true

    if (exists) {
    return target[name]
    } else {
    return get.value.call(target, name)
    }
    }
    }

    // __set()
    // Catches "instance.property = ..."
    const set = Object.getOwnPropertyDescriptor(clazz.prototype, '__set')
    if (set) {
    instanceHandler.set = (target, name, value) => {
    if (name in target) {
    target[name] = value
    } else {
    return target.__set.call(target, name, value)
    }
    }
    }

    // __isset()
    // Catches "'property' in instance"
    const isset = Object.getOwnPropertyDescriptor(clazz.prototype, '__isset')
    if (isset) {
    instanceHandler.has = (target, name) => {
    if (!issetEnabled) return name in target

    return isset.value.call(target, name)
    }
    }

    // __unset()
    // Catches "delete instance.property"
    const unset = Object.getOwnPropertyDescriptor(clazz.prototype, '__unset')
    if (unset) {
    instanceHandler.deleteProperty = (target, name) => {
    return unset.value.call(target, name)
    }
    }

    return new Proxy(instance, instanceHandler)
    }

    // __getStatic()
    // Catches "class.property"
    if (Object.getOwnPropertyDescriptor(clazz, '__getStatic')) {
    classHandler.get = (target, name) => {
    if (name in target) {
    return target[name]
    } else {
    return target.__getStatic(name)
    }
    }
    }

    // __setStatic()
    // Catches "class.property = ..."
    if (Object.getOwnPropertyDescriptor(clazz, '__setStatic')) {
    classHandler.set = (target, name, value) => {
    if (name in target) {
    return target[name]
    } else {
    return target.__setStatic(name, value)
    }
    }
    }

    return new Proxy(clazz, classHandler)
    }
    69 changes: 69 additions & 0 deletions magic-methods.md
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,69 @@
    # JavaScript Magic Methods
    This script implements some of PHP's magic methods for JavaScript classes, using a [Proxy](https://developer.mozilla.org/de/docs/Web/JavaScript/Reference/Global_Objects/Proxy).

    ## Example
    You can use it like this:
    ```javascript
    const Foo = magicMethods(class Foo {
    constructor () {
    this.bar = 'Bar'
    }

    __get (name) {
    return `[[${name}]]`
    }
    })

    const foo = new Foo
    foo.bar // "Bar"
    foo.baz // "[[baz]]"
    ```

    If you're using a JavaScript transpiler like Babel with decorators enabled, you can also use the `magicMethods` function as a decorator:

    ```javascript
    @magicMethods
    class Foo {
    // ...
    }
    ```

    ## Supported Magic Methods
    Given a class `Class` and an `instance` of it, the following are the magic methods supported by this script:

    ### `__get(name)`
    Called when trying to access `instance[name]` where `name` is neither set as a property of `instance`.

    **Attention:** As in PHP, the check if `name` exists in `instance` does not use any custom `__isset()` methods.

    ### `__set(name, value)`
    Called when trying to do `instance[name] = ...` where `name` is neither set as a property of `instance`.

    ### `__isset(name)`
    Called when trying to check existance of `name` by calling `name in instance`.

    ### `__unset(name)`
    Called when trying to unset property `name` by calling `delete instance[name]`.

    ## Additional Methods
    The following magic methods are made available by this script, but are not supported in PHP:

    ### `static __getStatic(name)`
    Like `__get()`, but in the `Class` instead of the `instance`.

    ### `static __setStatic(name, value)`
    Like `__set()`, but in the `Class` instead of the `instance`.

    ## Why is Magic Method `X` not supported?
    They are either not necessary or not practical:

    * `__construct()` is not needed, there's JavaScript's `constructor` already.
    * `__destruct()`: There is no mechanism in JavaScript to hook into object destruction.
    * `__call()`: As opposed to PHP, methods are just like properties in JavaScript and are first obtained via `__get()`. To implement `__call()`, you simply return a function from `__get()`.
    * `__callStatic()`: As in `__call()`, but with `__getStatic()`.
    * `__sleep()`, `__wakeup()`: There's no builtin serialization/unserialization in JavaScript. You could use `JSON.stringify()`/`JSON.parse()`, but there's no mechanism to automatically trigger any methods with that.
    * `__toString()` is already present in JavaScript's `toString()`
    * `__invoke()`: JavaScript will throw an error if you'll try to invoke a non-function object, no way to avoid that.
    * `__set_state()`: There's nothing like `var_export()` in JavaScript.
    * `__clone()`: There's no builtin cloning functionality in JavaScript that can be hooked into.
    * `__debugInfo()`: There's no way to hook into `console.log()` output.