Skip to content

Instantly share code, notes, and snippets.

@ohjay666
Forked from loilo/magic-methods.js
Last active November 21, 2023 16:22
Show Gist options
  • Select an option

  • Save ohjay666/ba442a4f72e2db2205b0bd111df83b8e to your computer and use it in GitHub Desktop.

Select an option

Save ohjay666/ba442a4f72e2db2205b0bd111df83b8e to your computer and use it in GitHub Desktop.

Revisions

  1. ohjay666 revised this gist Nov 21, 2023. 1 changed file with 3 additions and 4 deletions.
    7 changes: 3 additions & 4 deletions magic-methods.js
    Original file line number Diff line number Diff line change
    @@ -7,8 +7,6 @@ function magicMethods (clazz) {

    // Trap for class instantiation
    classHandler.construct = (target, args, receiver) => {
    // Variable to keep the proxy object created
    var instanceProxy;
    // Wrapped class instance
    const instance = Reflect.construct(target, args, receiver)

    @@ -66,8 +64,9 @@ function magicMethods (clazz) {
    return unset.value.call(target, name)
    }
    }
    // assign instanceProxy to be uses in get trap when fired
    return instanceProxy = new Proxy(instance, instanceHandler)
    // keep instanceProxy to be used in get trap when __get method is called
    const instanceProxy = new Proxy(instance, instanceHandler)
    return instanceProxy
    }

    // __getStatic()
  2. ohjay666 revised this gist Nov 21, 2023. 1 changed file with 1 addition and 2 deletions.
    3 changes: 1 addition & 2 deletions readme.md
    Original file line number Diff line number Diff line change
    @@ -23,8 +23,7 @@ const Foo = magicMethods(class Foo {
    })

    const foo = new Foo
    foo.bar // "Bar"
    foo.baz // "[[baz]]"
    foo.fullName; // Petra Mustermann
    ```

    If you're using a JavaScript transpiler like Babel with decorators enabled, you can also use the `magicMethods` function as a decorator:
  3. ohjay666 revised this gist Nov 21, 2023. 1 changed file with 1 addition and 0 deletions.
    1 change: 1 addition & 0 deletions readme.md
    Original file line number Diff line number Diff line change
    @@ -1,5 +1,6 @@
    # 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).

    The fork contains a modification that makes the __get method usable recursively like in php

    ## Example
  4. ohjay666 revised this gist Nov 21, 2023. 2 changed files with 13 additions and 4 deletions.
    8 changes: 5 additions & 3 deletions magic-methods.js
    Original file line number Diff line number Diff line change
    @@ -7,6 +7,8 @@ function magicMethods (clazz) {

    // Trap for class instantiation
    classHandler.construct = (target, args, receiver) => {
    // Variable to keep the proxy object created
    var instanceProxy;
    // Wrapped class instance
    const instance = Reflect.construct(target, args, receiver)

    @@ -27,7 +29,7 @@ function magicMethods (clazz) {
    if (exists) {
    return Reflect.get(target, name, receiver)
    } else {
    return get.value.call(target, name)
    return get.value.call(instanceProxy, name)
    }
    }
    }
    @@ -64,8 +66,8 @@ function magicMethods (clazz) {
    return unset.value.call(target, name)
    }
    }

    return new Proxy(instance, instanceHandler)
    // assign instanceProxy to be uses in get trap when fired
    return instanceProxy = new Proxy(instance, instanceHandler)
    }

    // __getStatic()
    9 changes: 8 additions & 1 deletion readme.md
    Original file line number Diff line number Diff line change
    @@ -1,5 +1,6 @@
    # 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).
    The fork contains a modification that makes the __get method usable recursively like in php

    ## Example
    You can use it like this:
    @@ -10,7 +11,13 @@ const Foo = magicMethods(class Foo {
    }

    __get (name) {
    return `[[${name}]]`
    switch (name){
    case 'firstName': return 'Petra';
    case 'lastName': return 'Mustermann';
    // __get also fires within __get
    case 'fullName': return this.firstName + ' ' + this.lastName;
    }
    console.error('undefined prop ' + name);
    }
    })

  5. @loilo 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 {}
    ```

  6. @loilo 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()`
  7. @loilo 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
    ```
  8. @loilo 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
    ```
  9. @loilo 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.

  10. @loilo 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)

  11. @loilo 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)
  12. @loilo 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 {
    // ...
    })
    ```
  13. @loilo 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.
  14. @loilo loilo renamed this gist Feb 28, 2018. 1 changed file with 0 additions and 0 deletions.
    File renamed without changes.
  15. @loilo 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.