Skip to content

Instantly share code, notes, and snippets.

@taufik-nurrohman
Forked from loilo/magic-methods.js
Created May 6, 2019 10:37
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save taufik-nurrohman/23a1fd56b4668863177c93eb22510185 to your computer and use it in GitHub Desktop.
Save taufik-nurrohman/23a1fd56b4668863177c93eb22510185 to your computer and use it in GitHub Desktop.
PHP Magic Methods in JavaScript

JavaScript Magic Methods

This script implements some of PHP's magic methods for JavaScript classes, using a Proxy.

Example

You can use it like this:

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:

@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 not an existing 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.

Can I extend a class with Magic Methods on it?

Yes, to a certain extent:

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:

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
function magicMethods (clazz) {
// A toggle switch for the __isset method
// Needed to control "prop in instance" inside of getters
let issetEnabled = true
const classHandler = Object.create(null)
// Trap for class instantiation
classHandler.construct = (target, 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, receiver) => {
if (name in target) {
return target[name]
} else {
return target.__getStatic.call(receiver, name)
}
}
}
// __setStatic()
// Catches "class.property = ..."
if (Object.getOwnPropertyDescriptor(clazz, '__setStatic')) {
classHandler.set = (target, name, value, receiver) => {
if (name in target) {
return target[name]
} else {
return target.__setStatic.call(receiver, name, value)
}
}
}
return new Proxy(clazz, classHandler)
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment