Pratphall is an optionally-typed language that compiles to readable PHP. PHP has a powerful engine and is very scalable since state is primarily per-request for PHP scripts. Unfortunately, due to the dynamic nature of PHP, it can be difficult to build large projects and retain the ability to refactor and manage the code. It can also be difficult to foresee errors with typing and unavailable variables. Almost all of these issues cause many problems that don't surface until runtime. Pratphall aims to solve this.
Pratphall started as kind of a joke (hence the name) and proof of concept to see if TypeScript could be reliably translated. It turns out it can. So Pratphall is for all practical purposes just TypeScript (which is a typed superset of JavaScript) cross-compiled. And although not every single JS idiom directly translates, remembering that Pratphall is just TypeScript which is just typed JavaScript will help development greatly.
//normal array
var arr = [1, 2, 3, 4, 5];
//loop and print
arr.forEach((value: number) => {
echo('My value: ' + value + '\n');
});
//compile-time-only class
interface SampleContract extends Pct.CompileTimeOnly {
requiredProperty: string;
optionalProperty?: bool;
}
//function using compile-time class
function printInfo(info: SampleContract) {
echo('Required: ' + info.requiredProperty + '\n');
if (typeof info.optionalProperty !== 'undefined') {
echo('Optional: ' + info.optionalProperty + '\n');
}
}
//type inferred matching
printInfo({ requiredProperty: 'required val' });
//normal array
$arr = [1, 2, 3, 4, 5];
//loop and print
foreach ($arr as $value) {
echo('My value: ' . $value . "\n");
}
/*Spacing added in example for clarity*/
//function using compile-time class
function printInfo($info) {
echo('Required: ' . $info->requiredProperty . "\n");
if (array_key_exists('optionalProperty', $info)) {
echo('Optional: ' . $info->optionalProperty . "\n");
}
}
//type-inferred matching
printInfo((object)[ 'requiredProperty' => 'required val' ]);
To use Pratphall from the command line, simply install via NPM (it's a little large since it includes source for metaprogramming facilities):
npm install -g pratphall
You may also want to install TypeScript for Visual Studio 2012 if you want to use Visual Studio as your editor. For more information about Visual Studio support, see below. If using Pratphall in an editor, you may not want to use the -g option which installs globally; this way you can directly reference node_modules/pratphall/bin/php.d.ts in your source.
If you want to build Pratphall from source, see the Building From Source appendix.
Write the following simple script and save it as sayHello.ts:
function sayHello(subject: string) {
return 'Hello, ' + subject;
}
var theSubject = 'World';
echo(sayHello(theSubject) + '\n');
Now, to compile to PHP, run ppc like so:
ppc sayHello.ts
This will create a sayHello.php file that looks like this:
function sayHello($subject) {
return 'Hello, ' . $subject;
}
$theSubject = 'World';
echo(sayHello(theSubject) . "\n");
Running this with PHP will output what you expect. However, if you tried to compile this:
function sayHello(subject: string) {
return 'Hello, ' + subject;
}
var theSubject = 123;
echo(sayHello(theSubject) + '\n');
You would get: sayHello.ts(7,5): Supplied parameters do not match any signature of call target
. This is because you are attempting to pass a number to a function that only accepts strings.
Nobody really. It's a project that was started in my spare time and only maintained in my spare time. There is no commercial support, no guarantee that anything will get fixed, and no guarantee that the project will be properly maintained. The only project known to use Pratphall is dust-php. If Pratphall becomes popular, it is possible it could become a more professional language. Feel free to discuss the language or ask questions on the mailing list.
Pratphall is TypeScript compiled to PHP. It includes several helpers to make this easier. Pratphall's code is licensed under the MIT license, but the Microsoft TypeScript code within (referenced as a submodule) is licensed under the Apache 2 license.
The source is on GitHub and the site is on GitHub. Follow development, fork, send pull requests, and submit issues there.
I don't know. It's in an almost alpha state until it gets more traction.
This is basically for static typing enthusiasts that use PHP either because they are forced to or like the PHP engine and scalability. Using PHPDoc annotations is rarely a good enough solution and editor support for them is limited. After noticing extreme parity between TypeScript and PHP, a simple tool was built to do the cross-compiling. This is the result.
Pratphall can be used to make a really scalable codebase in PHP. It can also reduce verbosity (e.g. several classes/modules in one file) and increase productivity through editor support (e.g. Visual Studio with IntelliSense). Refactoring and determining use throughout the your codebase will now be really easy. And, if the abstraction is written properly, code can even be shared between JS/PHP. At the very least this really helps when defining typed model classes.
Pratphall simply leverages the TypeScript compiler and type checker. Because TypeScript is written so well, it is very easy to change the language it emits to. In this case, a new emitter was written to just write PHP instead of JavaScript.
ppc [options] FILE...
Options:
-c
, --config FILE
--exclude-outside
--ext FILE
--force-block
--function-brace-newline
-h
, --help
--indent-spaces COUNT
--indent-tabs
--lint
--no-comments
--no-js-lib
--no-organize
--no-php-lib
--no-type-hint
-o
, --out PATH
--single
is present or there is only a single file on the command line, this is the single file to output to. References in the file are emitted relative to this single file path when not using --single
. Otherwise, this is the directory to put all files at relative to the files given.--prefer-single-quotes
--require-references
///<reference />
will become require_once
. This is only allowed when --no-organize
is set.--single
--no-organize
also, so both cannot be present.--type-brace-newline
--use-else-if
--verbose
-w
, --watch
--single
this means files and directories created during compilation may be deleted on later compilation. Files are not overwritten if their output contents didn't change. Finally note this does not always work over remotely mounted filesystems.--watch-debounce-ms MS
Booleans and conversions happen the same way in Pratphall as they do in PHP.
Strings in Pratphall and PHP are somewhat similar. There are no multiline strings. There is no string interpolation (attempts are escaped). Usually, the preference of single quote or double quote in the Pratphall script is translated verbatim to PHP. However, if there is an escape character in a single-quoted string, it will become double quoted. To request that all strings without escape characters to become single quoted in PHP, use the --prefer-single-quotes
compiler option.
In Pratphall strings are concatenated with a plus sign. In PHP the dot operator is used. Type inference is used to determine when this translation should occur. In Pratphall, if either side of the plus operator is a string, concatenation is assumed. If either side of a plus operator is an unknown/any type a normal arithmetic plus will be used and a compiler warning will be issued.
Normal JavaScript string functions are translated to common PHP idioms with some gotchas.
//will be converted to double quotes
var a = 'My\nNewline\nString$escaped{$interpolation}';
//concatenation
var b = 99 + ' bottles of beer on the wall';
//compiler warning because type is any
var c: any = 'test string';
var d = 12 + c;
//common JS functions
var_dump(b.length);
var_dump(b.charAt(0));
var_dump(b.split(' '));
//will be converted to double quotes
$a = "My\nNewline\nString\$escaped{\$interpolation}";
//concatenation
$b = 99 . ' bottles of beer on the wall';
//compiler warning because type is any
$c = 'test string';
$d = 12 + $c;
//common JS functions
var_dump(strlen($b));
var_dump($b[0]);
var_dump(explode(' ', $b));
In JS there is no difference between an integer and a float for the most part, and the same is true in Pratphall. Conversion and testing can still occur at runtime with intval, floatval, and other methods. At compile time however, they are simply numbers. There is no normal way to constrain a variable on one type or the other. Hexadecimal and octal literals are translated to normal decimal integers during translation.
Unlike PHP, Pratphall differentiates between associative arrays and normal indexed arrays. The former is of type Pct.PhpAssocArray
whereas the latter is a normal JS array. This can be confusing at first, but in practice it helps the developer make more accurate assumptions about the typing of variables. You can convert between these types at compile time using Pct.toAssocArray
or Pct.toArray
. Note when using the latter, (until TypeScript has generics) the result is any[]
so it is recommended to cast it to the exact form of array expected.
Associative arrays can be created with Pct.newAssocArray
. Only object literals can be passed to the function. Indexed arrays are handled almost exactly like they are in JS. They are created with the normal bracketed syntax.
Normal JavaScript array functions are translated to common PHP idioms with a few gotchas.
//regular array
var a = [1, 2, 3];
//some calls
a.forEach((value: number) => {
var_dump(value);
});
a.push(4);
//associative array
var b = Pct.newAssocArray({
prop: 'propValue',
prop2: ['a', 'b', 'c'],
closure: function () { echo('Hey!\n'); }
});
//some calls
var c = b.map((value: any) => {
return gettype(value);
});
c['prop3'] = 15;
var_dump(c.length);
//regular array
$a = [1, 2, 3];
//some calls
foreach ($a as $value) {
var_dump($value);
}
$a[] = 4;
//associative array
$b = [
'prop' => 'propValue',
'prop2' => ['a', 'b', 'c'],
'closure' => function () { echo("Hey!\n"); }
];
//some calls
$c = array_map(function ($value) {
return gettype($value);
}, $b);
$c['prop3'] = 15;
var_dump(count($c));
Objects in Pratphall and PHP work very similarly. Anonymous object literals become an stdClass via cast. Note, if you have a closure as an object property, it cannot necessarily be invoked like a method in PHP, so Pratphall has to do special handling (see GOTCHA-001). TypeScript property accessors are not currently supported.
var obj = {
num: 12,
arr: ['a', 'b', 'c'],
obj: { innerNum: 12 },
func: (value: string) => { echo('Value: ' + value); }
};
$obj = (object)[
'num' => 12,
'arr' => ['a', 'b', 'c'],
'obj' => (object)[ 'innerNum' => 12 ],
'func' => function ($value) { echo('Value: ' . $value); }
];
PHP resources are represented by the Pct.PhpResource
type in Pratphall.
Null is handled the same way in Pratphall and PHP.
Undefined is different. In Pratphall, delete
translates directly to unset
. Conditions that check typeof undefined
are translated to safe PHP that avoids false positives with NULL values unlike isset.
Functions isset and empty are used like any other PHP function in Pratphall. Since these are language constructs, the values don't necessarily have to exist like they do in JS.
//normal null
var a = { prop1: null };
echo('a.prop1 null? ' + (a.prop1 == null));
//undefined
delete a.prop1;
echo('a.prop1 set? ' + (typeof a.prop1 == 'undefined'));
//normal null
$a = (object)[ 'prop1' => null ];
echo('a.prop1 null? ' . ($a->prop1 == null));
//undefined
unset($a->prop1);
echo('a.prop1 set? ' . (!array_key_exists('prop1', $a)));
Pratphall supports full typed closure type definitions instead of just the callable type hint in PHP. Even methods of objects can be passed around like normal.
class Foo {
bar(value: string) { echo('Value: ' + value); }
}
var foo = new Foo();
var f = foo.bar;
//call
f('test');
class Foo {
public function bar($value) { echo('Value: ' . $value); }
}
$foo = new Foo();
$f = (new \ReflectionMethod('Foo', 'bar'))->getClosure($foo);
//call
$f('test');
This is safe since unlike PHP, Pratphall does not support naming properties/variables and methods/functions the same name. See GOTCHA-002 about interoperating with existing PHP that does.
Void can be used for return types of functions. There is no such thing as mixed
, but there is any
which loosely translates. The use of the any
type should be avoided when possible. If using any
, you are encouraged to cast to a known type as soon as you can.
To cast to a value at compile time, use the TypeScript method. For example: <bool><any>stringValue
casts a string to a bool but will not be casted in the emitted PHP code. This is helpful for functions that return a value or FALSE. In fact, this is such a common pattern that there are compile-time helpers for it: Pct.isFalse
and Pct.isNotFalse
.
To cast a value at runtime, it is always preferred to use the built-in PHP methods such as `strval`, `intval`, etc. If you must have a literal cast, you can use the `Pct.cast*` functions.
function printNumber(value: number) {
echo('Number: ' + value + '\n');
}
//compile-time cast only
printNumber(<number><any>'10');
//runtime PHP function cast
printNumber(intval('10'));
//runtime PHP explicit cast
printNumber(Pct.castInt('10'));
//two ways to check a FALSE return
if (<bool><any>strpos('team', 'I') === false) {
echo('There is no "I" in "team"\n');
}
if (Pct.isFalse(strpos('team', 'I'))) {
echo('There is no "I" in "team"\n');
}
function printNumber($value) {
echo('Number: ' . $value . "\n");
}
//compile-time cast only
printNumber('10');
//runtime PHP function cast
printNumber(intval('10'));
//runtime PHP explicit cast
printNumber(((int) '10'));
//two ways to check a FALSE return
if (strpos('team', 'I') === false) {
echo("There is no \"I\" in \"team\"\n");
}
if (strpos('team', 'I') === false) {
echo("There is no \"I\" in \"team\"\n");
}
Variables names are case sensitive and are NOT prefixed with dollar signs (except for references, see later).
All variables in Pratphall are defined somewhere. If they are declared globally and are used in a function, the global
keyword is applied in the translated PHP. There is no Pratphall equivalent to the static variable in PHP (not to be confused with the static property/function of a class).
var a = 'myGlobalString';
function printGlobalString() {
/*blank space here for clarity*/
echo('Global string: ' + a);
}
$a = 'myGlobalString';
function printGlobalString() {
global $a;
echo('Global string: ' + $a);
}
"Variable variables" are not supported in Pratphall in any way. The best you can do is access an object's property by its string-based name using the bracket syntax.
var obj = { child: { grandchild: 5 }};
echo('Grandchild val: ' + obj.child['grand' + 'child']);
$obj = (object)[ 'child' => (object)[ 'grandchild' => 5 ]];
echo('Grandchild val: ' . $obj->child->{'grand' . 'child'});
By default in Pratphall, variable names that are completely capitalized are assumed to be constant and variables that are not are assumed to be normally named. To override this behavior, use Pct.const
or Pct.asVar
when referencing the variable to change Pratphall's default. Note, the superglobals GLOBALS
, _SERVER
, _GET
, _POST
, _FILES
, _COOKIE
, _SESSION
, _REQUEST
, and _ENV
are all automatically known by Pratphall to be prefixed with dollar signs.
All operators work the same in Pratphall and PHP with the following exceptions:
clone
Pct.clone
var obj = { foo: 'bar' };
var copy = Pct.clone(obj);
$obj = (object)[ 'foo' => 'bar' ];
$copy = clone $obj;
.
+
and translated based on type inference. Same goes for .=
in PHP which is +=
in Pratphall.
and
, xor
, or
&&
and ||
).>>>
?:
&
<>
@
@
symbol may not be used to swallow errors in Pratphall. If you must swallow errors, use Pct.swallowErrors
(unwieldy name on purpose) which will use the @
sign.
var f = Pct.swallowErrors(file('not-here.txt'));
$f = @file('not-here.txt');
`
+
Pct.unionArray
. Note, this comes back with any[]
if you are using an indexed array, so please cast immediately back to what you expect it to be.
var a = <number[]>Pct.unionArray([1, 2, 3], [4, 5, 6, 7]);
$a = [1, 2, 3] + [4, 5, 6, 7];
==
, !=
, ===
instanceof
Pct.isInstance
. Of course, is_a
may be also be used as part of the PHP standard library.
class Foo { }
var a = new Foo();
var b = Pct.isInstance(a, 'Foo');
class Foo { }
$a = new Foo();
$b = a instanceof 'Foo';
typeof
All control structures work the same in Pratphall and PHP with the following exceptions:
elseif
else if
in Pratphall which will translate to elseif
in PHP by default. The --use-else-if
compiler option can be used to emit else if
instead.
if():
, elseif:
, else:
, endif;
, while:
, endwhile;
, for:
, endfor;
, foreach:
, endforeach;
, switch:
, endswitch;
for...in
array_keys
of an array cast which is not the exact same thing. These loops are discouraged.
var a = { b: 1, c: 2 };
for (b in a) {
echo('a[' + b + ']: ' + a[b] + '\n');
}
$a = (object)[ 'b' => 1, 'c' => 2 ];
foreach (array_keys($a) as $b) {
echo('a[' . $b . ']: ' . $a->{$b} . "\n");
}
foreach
forEach
function that can be called on an iterable object or an array. If the function passed to forEach
is an inline closure, it will be translated directly. Otherwise, it will be translated to an array_walk
call.
var a = Pct.newAssocArray({ b: 1, c: 2 });
//regular
a.forEach((value: number, index: string) => {
echo('a[' + index + ']: ' + value + '\n');
});
//callback
var d = (value: number, index: string) => {
echo('a[' + index + ']: ' + value + '\n');
};
a.forEach(d);
$a = (object)[ 'b' => 1, 'c' => 2 ];
//regular
foreach ($a as $index => $value) {
echo('a[' + $index + ']: ' + $value + "\n");
}
//callback
$d = function ($value, $index) {
echo('a[' + $index + ']: ' + $value + "\n");
};
array_walk($a, $d);
break #
, continue #
var counter = 0;
again:
var lines = file('file' + (++counter) + '.txt');
for (var i = 0; i < lines.length; i++) {
var words = lines[i].split(' ');
for (var j = 0; j < words.length; j++) {
var word = words[j];
if (word == 'end') break done;
else if (word == 'nextfile') break again;
else echo('Word: ' + word + '\n');
}
}
done:
echo('Done!\n');
$counter = 0;
again:
$lines = file('file' + (++$counter) + '.txt');
for ($i = 0; $i < count($lines); $i++) {
$words = explode(' ', lines[$i]);
for ($j = 0; $j < count($words); $j++) {
$word = $words[$j];
if ($word == 'end') goto done;
elseif ($word == 'nextfile') goto again;
else echo('Word: ' . $word . "\n");
}
}
done:
echo("Done!\n");
declare
Pct.declare
. If a function is the last parameter of the call, it considers code within to be in a declare block.
return
include
, include_once
, require
, require_once
///<reference />
elements at the top of your file. If the --require-references
compiler option is set, reference elements will become require_once
calls.
goto
Pratphall recommends avoiding references. If you cannot, they are supported.
To assign by reference or otherwise reference, use Pct.byRef
.
To pass by reference, the parameter in the function must start with a dollar sign. A compiler error occurs if the item passed by reference is not valid (e.g. expressions or literals).
To return by reference, the function name must start with a dollar sign. Also, to receive the returned result as a reference, you must call Pct.byRef
.
//assign
var a = 'hey';
var b = Pct.byRef(a);
//pass
function addWord($val: string) {
$val += ' word';
}
addWord(b);
echo('Changed? ' + (a == 'hey word'));
//return
var c = { d: 15 };
function $getReference(val: { d: number; }) {
return val.d;
}
var e = Pct.byRef($getReference(c));
e = 20;
echo('Changed? ' + (c.d == 20));
//assign
$a = 'hey';
$b = &$a;
//pass
function addWord(&$val) {
$val .= ' word';
}
addWord($b);
echo('Changed? ' . ($a == 'hey word'));
//return
$c = (object)['d' => 15];
function &getReference($val) {
return $val->d;
}
$e = &getReference($c);
$e = 20;
echo('Changed? ' . ($c->d == 20));
Functions in Pratphall behave very similarly to PHP.
Overloads declarations are allowed in Pratphall like they are in TypeScript, but only one "catch all" function with an implementation is allowed. The overloaded signatures will be discarded.
Type hinting is done by default for arrays, callables, and actual type declarations. Parameters who are typed with compile time only types (see compile time declarations below) will not be type hinted. If the compiler option --no-type-hint
is set, type hinting will not be present on any function.
Pratphall directly supports typed variadic function arguments (known as "RestParameters" in the TypeScript specification) whereas PHP does not. The use of variadic functions is not encouraged, but supported.
When the arguments
array is referenced in a function, it is translated to PHP by using func_get_args
(see GOTCHA-003 about re-defining arguments). When a rest parameter is used as a parameter in a function, it is derived from func_get_args
function myprint(one: string, ...args: any[]) {
/* left blank to make comparison easier */
echo('First: ' + arguments[0] + '\n');
echo('Last: ' + arguments[arguments.length - 1] + '\n');
if (args.length > 0) {
echo('First args: ' + args[0] + '\n');
echo('Last args: ' + args[args.length - 1] + '\n');
}
}
myprint('foo', true, 13.5, 'bar');
function myprint($one) {
$arguments = func_get_args();
$args = array_slice(func_get_args(), 1);
echo('First: ' . $arguments[0] + "\n");
echo('Last: ' . $arguments[count($arguments) - 1] . "\n");
if (count($args) > 0) {
echo('First args: ' . $args[0] . '\n');
echo('Last args: ' . $args[count($args) - 1] . "\n");
}
}
myprint('foo', true, 13.5, 'bar');
Both PHP and Pratphall support optional function parameters. PHP supports only default parameters with only scalars/arrays and they can be anywhere within the parameter list. Pratphall supports both optional and default parameters which can be initialized to any type and must be at the end of the parameter list.
Optional parameters in Pratphall without a default value are assumed to be null. If the default parameter is specified and it is not a constant, scalar value, it will be set at the beginning of the function (this practice is usually discouraged).
function words(prefix?: bool, file = file('default.txt')) {
/* left blank to make comparison easier */
file.forEach((line: string) => {
line.split(' ').forEach((word: string) => {
if (prefix) echo('Word: ');
echo(word + '\n');
});
});
}
//reads default.txt and prints w/out prefix
words();
//uses array and prints w/ prefix
words(true, ['foo bar', 'fu bar']);
function words($prefix = null, $file = null) {
if ($file === null) $file = file('default.txt');
foreach ($file as $line) {
foreach (explode(' ', $line) as $word) {
if ($prefix) echo('Word: ');
echo($word . "\n");
}
}
}
//reads default.txt and prints w/out prefix
printWords();
//uses array and prints w/ prefix
printWords(true, ['foo bar', 'fu bar']);
Pratphall and PHP have support for anonymous functions with a common syntax and are invoked and passed the same way.
PHP's parser supports nested functions, but it places them in the global scope. Pratphall does NOT support these nested functions. When Pratphall encounters a nested function, it treats it like an anonymous function assigned to the variable of the same name. This means the compiler will error if a variable and nested function appear in the same function with the same name.
function func() {
var anonFunc = (value: string) => { echo(value); }
var anonFunc2 = function (value: string) { echo(value); }
function nestedFunc(value: string) { echo(value); }
}
function func() {
$anonFunc = function ($value) { echo($value); };
$anonFunc2 = function ($value) { echo($value); };
$nestedFunc = function ($value) { echo($value); };
}
Pratphall's (ahem, JS's) native try/catch syntax is much more limited than PHP's. It is supported and will even emit PHP 5.5 finally statements. You can compile-time cast the catch variable or do an instanceof check.
try {
new ReflectionClass('NonExistentClass');
} catch (err) {
if (err instanceof ReflectionException) {
echo('Not found: ' + (<Exception>err).getMessage());
} else {
echo('Other: ' + (<Exception>err).getMessage());
}
} finally {
echo('This runs always in PHP 5.5');
}
try {
new ReflectionClass('NonExistentClass');
} catch (Exception $err) {
if ($err instanceof ReflectionException) {
echo('Not found: ' . $err->getMessage());
} else {
echo('Other: ' . $err->getMessage());
}
} finally {
echo('This runs always in PHP 5.5');
}
Pratphall does include compile-time helpers to support the full try/catch syntax via Pct.try
. It is an object literal w/ a try function, an optional catch function (or array of functions), and an optional finally function. If neither a catch nor finally is supplied, it assumes an empty catch statement.
Pct.try({
try: () => {
new ReflectionClass('NonExistentClass');
},
catch: [
(err: ReflectionException) => {
echo('Not found: ' + err.getMessage());
},
(err: Exception) => {
echo('Other: ' + err.getMessage());
}
],
finally: () => {
echo('This runs always in PHP 5.5');
}
});
try {
new ReflectionClass('NonExistentClass');
} catch (ReflectionException $err) {
echo('Not found: ' . $err->getMessage());
} catch (Exception $err) {
echo('Other: ' . $err->getMessage());
} finally {
echo('This runs always in PHP 5.5');
}
Pratphall supports classes and interfaces just like PHP. There are several features that Pratphall does not support:
Pct.const
or Pct.asVar
when referencing them. (future possibility, see TS-368)
self::
or static::
to reference static members. Instead, you must use the class name explicitly. This means there is no late static binding. (future possibility, have a compile-time helper like Pct.self
or Pct.static
)
constructor
in Pratphall which translates to __construct
. A member named __construct
in Pratphall is an error. Also, using public or private before parameter declarations in a constructor automatically make them properties.
super
which is translated to parent
in PHP.
__invoke
and it will automatically be removed. Until bare call signature implementations are supported in classes, you cannot call the class as a function directly.
toString
becomes __toString
. A member named __toString
in Pratphall is an error.
export
is not placed on an interface or class, it is not visible outside the module it was declared in. This is a compile-time only feature and does not affect PHP output.
__construct
and __toString
are supported as normal methods. If they are not public, an error occurs. Be advised, the compile-time type system is unaware of runtime-derived members from things like __get
, __callStatic
, etc. Pratphall discourages the use of these magic methods, but if you must, cast to any first then cast the result back.
//spacing added below for clarity
interface Iface {
numberProperty: number;
write(param: string);
notRequired?(...params: any[]): bool;
}
class BaseClass {
static fileContents = file_get_contents('myfile.txt');
constructor(public numberProperty: number) {
echo('Constructed!')
}
}
class MyClass extends BaseClass implements Iface {
static getFileContents() {
return BaseClass.fileContents;
}
constructor() {
super(12);
}
__destruct() { echo('Destructing!\n'); }
private secretFunction() { echo('Hello\n'); }
write(param: string) {
echo('Value: ' + param + '\n');
}
__invoke(someVal: any) {
echo('I have been invoked!\n');
return this.numberProperty + Pct.castInt(someVal);
}
__get(prop: string) { return 42; }
__set(prop: string, value: any) {
echo('Setting ' + prop + ' with ' + value);
}
toString() { return 'MyClass'; }
}
var myClass = new MyClass();
echo('Instance? ' + (myClass instanceof Iface) + '\n');
//call static
var a = MyClass.getFileContents();
//call normal
myClass.write('Hey');
//invoke
var b = myClass.__invoke(20);
var c = myClass.__invoke('15');
//trigger getter/setter
var d = <number>(<any>myClass).someProp;
(<any>myClass).someNewProp = 20;
//string version
var e = myClass.toString();
//spacing added below for clarity
interface Iface {
public function write($param);
}
class BaseClass {
public static $fileContents;
public $numberProperty;
public function __construct($numberProperty) {
$this->numberProperty = $numberProperty;
echo('Constructed!');
}
}
BaseClass::$fileContents = file_get_contents('myfile.txt');
class MyClass extends BaseClass implements Iface {
public static function getFileContents() {
return BaseClass::$fileContents;
}
public function __construct() {
parent::__construct(12);
}
public function __destruct() { echo("Destructing!\n"); }
private function secretFunction() { echo("Hello\n"); }
public function write($param) {
echo('Value: ' . $param . "\n");
}
public function __invoke($someVal) {
echo("I have been invoked!\n");
return $this->numberProperty + (int) $someVal;
}
public function __get($prop) { return 42; }
public function __set($prop, $value) {
echo('Setting ' . $prop . ' with ' . $value);
}
public function __toString() { return 'MyClass'; }
}
$myClass = new MyClass();
echo('Instance? ' . ($myClass instanceof Iface) . "\n");
//call static
$a = MyClass::getFileContents();
//call normal
$myClass->write('Hey');
//invoke
$b = $myClass(20);
$c = $myClass('15');
//trigger getter/setter
$d = $myClass->someProp;
$myClass->someNewProp = 20;
//string version
$e = strval($myClass);
Ambient class, interface, variable, and function declarations are not emitted with the PHP output. They are best suited for describing external code. To make a variable, class, or function ambient, simply use the declare
keyword before it. To make an interface ambient, make the interface explicitly extend Pct.Ambient
. Another way to make all of these ambient is to simply place them in a declaration file (which is a file that has the .d.ts
extension). For example, here are the contents of json.d.ts
which describes the JSON library:
var JSON_ERROR_CTRL_CHAR: number;
var JSON_ERROR_DEPTH: number;
var JSON_ERROR_NONE: number;
var JSON_ERROR_STATE_MISMATCH: number;
var JSON_ERROR_SYNTAX: number;
var JSON_ERROR_UTF8: number;
var JSON_BIGINT_AS_STRING: number;
var JSON_FORCE_OBJECT: number;
var JSON_HEX_AMP: number;
var JSON_HEX_APOS: number;
var JSON_HEX_QUOT: number;
var JSON_HEX_TAG: number;
var JSON_NUMERIC_CHECK: number;
var JSON_PRETTY_PRINT: number;
var JSON_UNESCAPED_SLASHES: number;
var JSON_UNESCAPED_UNICODE: number;
interface JsonSerializable {
jsonSerialize(): any;
}
function json_decode(json: string, assoc?: bool, depth?: number, options?: number): any;
function json_encode(value: any, options?: number): string;
function json_last_error(): number;
Since ambient types are still seen at runtime, they will be emitted as type hints. Compile-time declarations do not suffer from this. They are only there to enforce a certain contract and that's it. Only interfaces and classes can be marked compile-time only, and this is done by explicitly extending or implementing the Pct.CompileTimeOnly
interface. Just because a super class/interface implements Pct.CompileTimeOnly
doesn't mean it's children do, it must be explicitly stated.
interface Place extends Pct.CompileTimeOnly {
zip: string;
city?: string;
}
interface Weather extends Pct.CompileTimeOnly {
place: Place;
temperature: number;
}
function printWeather(weather: Weather) {
echo('It is ' + weather.temperature + ' in ');
if (property_exists(weather.place, 'city')) {
echo(weather.place.city);
} else echo(weather.place.zip);
}
printWeather({
place: {
zip: '75001',
city: 'Addison'
},
temperature: 85
});
printWeather({
place: {zip: '76020'},
temperature: 88
});
function printWeather($weather) {
echo('It is ' . $weather->temperature . ' in ');
if (property_exists($weather->place, 'city')) {
echo($weather->place->city);
} else echo($weather->place->zip);
}
printWeather((object)[
'place' => (object)[
'zip' => '75001',
'city' => 'Addison'
],
'temperature' => 85
]);
printWeather((object)[
'place' => (object)['zip' => '76020'],
'temperature' => 88
]);
Modules in Pratphall are directly translatable as namespaces in PHP.
Even though PHP namespaces cannot be nested, Pratphall modules can. When emitted to PHP, they are separated. The dots translate to slashes in PHP. When not in the global namespace, globally namespaced type references are automatically prefixed with a slash.
If a module is imported with an alias, it is translated to a use statement. If the alias is the same as the last module part, the as
alias in the use statement is not emitted. Currently, Pratphall can only import and alias other modules, there is no importing or aliasing of other classes/interfaces. The namespace
operator to access something in the same module is not present in Pratphall.
External module references that use import mod = module('modname')
format cause a compiler warning and are ignored for now. Use of the export feature to share items amongst modules/namespaces is strictly a compile-time feature and is not emitted to PHP.
If you must reference code in another file, you can use ///<reference path="PATH" />
. It is common practice to reference a file that might just be full of references to the other files in the project. This is just a compile-time reference unless both --no-organize
and --require-references
are set as compiler options.
Different compiler options affect how the files are outputted. Below are descriptions of the options and output PHP examples based on these two files:
//file1.ts
module MyModule {
export interface MyIface {
}
export module Sub {
export class MyClass implements MyModule.MyIface {
causeErr() {
throw new Exception('Error');
}
}
}
}
//file2.ts
///<reference path="file1.ts" />
import Sub = MyModule.Sub;
var a = new Sub.MyClass();
a.causeErr();
//MyModule/MyIface.php
namespace MyModule;
interface MyIface {
}
//MyModule/Sub/MyClass.php
namespace MyModule\Sub;
class MyClass implements MyModule\MyIface
public function causeErr() {
throw new \Exception('Error');
}
}
//file2.php
use MyModule\Sub;
$a = new Sub\MyClass();
$a->causeErr();
--no-organize
--single
since that option basically includes this option. This option allows top-level code in any file and allows any file to operate in the global namespace. This option is best used in conjunction with --require-references
. When that option is set, the ///<reference path="PATH" />
references become require_once
calls in PHP.
//file1.php
namespace MyModule {
interface MyIface {
}
}
namespace MyModule\Sub {
class MyClass implements MyModule\MyIface
public function causeErr() {
throw new \Exception('Error');
}
}
}
//file2.php
use MyModule\Sub;
$a = new Sub\MyClass();
$a->causeErr();
--single
--no-organize
.
//file2.php
namespace MyModule {
interface MyIface {
}
}
namespace MyModule\Sub {
class MyClass implements MyModule\MyIface
public function causeErr() {
throw new \Exception('Error');
}
}
}
namespace {
use MyModule\Sub;
$a = new Sub\MyClass();
$a->causeErr();
}
Pratphall includes the ability to write compile-time extensions that can emit PHP code, validate input, or do anything else. They are written in TypeScript (not Pratphall) and run during compilation in nodejs.
Extensions utilize the AST structure built in to the TypeScript code base. The most common use of extensions is to emit PHP that isn't available in Pratphall. In fact, all of the Pct
features are implemented as extensions and can be seen in the Pratphall source in the src/ext folder. For example, here is the extension source that turns toString
calls in Pratphall to strval
calls in PHP:
///<reference path='../pratphall.ts' />
module Pratphall {
import TS = TypeScript;
PhpEmitter.registerExtension({
name: 'Object.toString emitter',
description: 'Make it strval',
matcher: {
nodeType: [TS.NodeType.Call],
priority: 1,
propertyMatches: {
target: (value: TS.AST): bool => {
return value instanceof TS.BinaryExpression &&
value.nodeType == TS.NodeType.Dot &&
(<TS.BinaryExpression>value).operand2 instanceof TS.Identifier &&
(<TS.Identifier>(<TS.BinaryExpression>value).operand2).text == 'toString';
}
}
},
emit: (ast: TS.CallExpression, emitter: PhpEmitter): bool => {
emitter.write('strval(').emit((<TS.BinaryExpression>ast.target).operand1).write(')');
return true;
}
});
}
There are several things to note here about the extension API. First, is the reference to pratphall.ts. This is just a directory above where this extension is. However, in your project where you may reference Pratphall locally in your package.json, it might be at node_modules/pratphall/src/pratphall.ts. Note, at the present extensions cannot reference other arbitrary TypeScript, it should all be self contained in the extension file.
Next, this extension happens to be in the Pratphall module, but yours can be in any. The PhpEmitter class has a static method called register extension that takes a single object. This object must have a name, a description, a matcher object, and an emit function. The name and description are unused currently, but provide metadata about the extension.
The matcher is an object that matches AST so the emitter knows when to invoke the extension. The nodeType property is an array of node types this extension accepts. You will have to reference the TypeScript source code to see what these node types are. The priority property is a numeric value which defines the order in which the extension is executed. The higher the priority, the earlier it will be executed. All built-in Pratphall extensions use a priority of 1. Finally there is the propertyMatches property. This property is simply a collection of functions bound to names of properties on the AST object you are targeting. The function will be called with that property as the parameter and you must return true for it to be handled. In the above example, this checks that it is a dotted binary expression whose right hand side is the toString function (e.g. something.toString).
The emit function accepts the AST and the PHP emitter as parameters. Again, you may have to look at the TypeScript and Pratphall source to understand these. The emitter offers several functions to help emit PHP. If the function returns true, no more extensions for this AST node will be executed. If it returns false, other extensions down the priority chain will be executed. If no extensions match an AST node, it is emitted normally.
Let's say you wanted to make an extension that ran some code that was output buffered and then flushed the buffer at the end. In PHP you do this with ob_start
and ob_end_flush
. On the Pratphall side, you will want to create a compile-time function to help with this. Then you can use it in Pratphall:
//create a module with a declared (ambient) function
//NOTE: this could also be a class w/ a static function or anything
module Output {
declare function bufferThenFlush(code: () => void);
}
//now use it
Output.bufferThenFlush(() => {
//buffer this!
echo('blah');
});
So now we must make an extension in TypeScript. Let's assume our extension is at src/myext.ts. Here is what it might look like:
///<reference path='../node_modules/pratphall/src/pratphall.ts' />
module MyExt {
import TS = TypeScript;
PhpEmitter.registerExtension({
name: 'Output.outputThenFlush',
description: 'Handle output-then-flush handler',
matcher: {
nodeType: [TS.NodeType.Call],
priority: 5,
propertyMatches: {
target: (value: TS.AST) => {
//isDottedBinEx is a helper available to you to check a dotted binary expression
return Pratphall.isDottedBinEx(value, 'Output', 'outputThenFlush');
}
}
},
emit: (ast: TS.CallExpression, emitter: PhpEmitter): bool => {
//first grab the argument
var arg = ast.arguments.members[0];
//let's make sure it's an anonymous function declaration
if (!(arg instanceof TS.FuncDecl) || !(<TS.FuncDecl>arg).isAnonymousFn()) {
emitter.addError(arg, 'Argument must be an anonymous function');
//return true saying we've handled this (will skip emit)
return true;
}
//let's start the output buffering
emitter.write('ob_start();').newline();
//now we need to take the body of the function and emit it directly
var func = <FuncDecl>arg;
//emitBlockStatements is a helper...otherwise, a normal emit() can be used for AST's
//the true means we want newlines before each statement
emitter.emitBlockStatements(func.bod, true);
//now end and flush
//notice we omit the ending semi-colon...that is because this was originally
// a statement and all statements receive a semi-colon already; so after this is
// written a semicolon and newline is automatically added just like if this extension
// was never executed
emitter.newline().newline().write('ob_end_flush()');
//return true to say we've handled it
return true;
}
});
}
That's all there is to it. Now when running ppc, you can pass --ext src/myext.ts
and the code that used the ambient function will look like this.
ob_start();
//buffer this!
echo('blah');
ob_end_flush();
Extensions are also a good way to write validators. Simply do your checks and add errors or warnings in the emit function. You can return true on errors to skip emitting or false to continue. There are many examples of extensions in the Pratphall source at src/ext. Even dust-php uses an extension in the test/ folder that takes some JSON and makes test cases out of it. It is a complex subject, but working through a few will make it easier to understand.
string|null
PHPDoc type).
finally
in your try blocks, then PHP 5.5 is required. This decision was made since Pratphall is a new language and should not be bound by older environments. It is possible that 5.3 will be supported with a compiler option if there is enough need.
Stronger built-in Visual Studio support will be coming soon in Pratphall, but for now it's fairly limited. Having said that, it is still the most complete IDE for Pratphall. Visual Studio Express 2012 for Web is free and it was the IDE used to create Pratphall. When using Visual Studio, it is recommended to use --no-php-lib when compiling and add a reference to php.d.ts (in the bin directory of installation) to a commonly referenced base. This will give intellisense support and all normal Visual Studio features.
There are a few reported errors from the TypeScript compiler that Pratphall suppresses. These will appear as errors in Visual Studio, but are not Pratphall errors. They are:
Despite the above, using Visual Studio is well worth it. Besides the refactoring and search capabilities, you also get intellisense:
Building from source is done very easily. You must have nodejs installed. Then, clone the git repository (using --recursive to get the TypeScript submodule):
git clone --recursive https://github.com/cretz/pratphall.gitNow navigate into the newly created pratphall directory and run npm install to resolve the development dependencies:
npm installThis will install Jake, TypeScript, and a few other items locally. Now run the build task with local Jake (or the global version if you already have Jake installed):
node_modules/.bin/jake buildThis will compile everything into a single JS and move everything to the bin directory. You can also run the test cases very easily:
node_modules/.bin/jake test
A configuration file can be passed in to ppc using the -c
or --config
option. It contains all the same features as the command line options, but sometimes worded differently. When a configuration file is specified, it overwrites any command line parameters that may appear before it. If there are command line parameters after the configuration file parameter, they will override configuration options. Here is the TypeScript class definition (with defaults) the JSON values are merged into:
class CompilerOptions {
//whether or not to emit comments on output
comments = true;
//whether or not to exclude emitting files above/outside
//of the input file's directory
excludeOutside = false;
//array of extension .ts files to be loaded
extensions: string[] = [];
//if true, makes all single line if, while, etc
//statements have a block for their contents
forceBlock = false;
//if true, forces the opening brace of any function
//(non closure) declaration to start on the next line
// by itself
functionBraceNewline = false;
//the number of times to indent per depth, usually best at 1
//if indentSpaces is false (meaning using tabs)
indentCount = 4;
//if true, indent with spaces; if false, indent with tabs
indentSpaces = true;
//whether or not to include TypeScript's lib.d.ts
jsLib = true;
//if true, parse/compile only, do not emit output files
lint = false;
//if true, organize source into PSR-0-style type-per-file
organize = true;
//the individual file or directory to output to
out: string = null;
//whether or not to include the php.d.ts runtime lib automatically
phpLib = true;
//if true, will always use single quotes unless there is
//an escape character
preferSingleQuotes = false;
//if true, Pratphall reference's will become require_once statements
requireReferences = false;
//if true, emits a single PHP file with all source
single = false;
//if true, forces the opening brace of any class or interface
//declaration to start on the next line by itself
typeBraceNewline = false;
//if false, do not emit type hints on any functions in the output
typeHint = true;
//if false, use "else if" instead of "elseif"
useElseif = true;
//if true, output lots of extra information during compile
verbose = false;
//if true, run as daemon, watching for changes on any referenced
//files and triggering a smart recompile
watch = false;
//the number of milliseconds after the last file change event
//to start the smart recompile
watchDebounceMs = 1500;
}
Here is an example of a configuration that complies mostly with PSR-2:
{
"forceBlock": true,
"functionBraceNewline": true,
"indentCount": 4,
"indentSpaces": true,
"organize": true,
"typeBraceNewline": true,
"useElseif": true
}
Below are gotchas that might not be expected during development
001: Pratphall can't tell whether it's invoking an object's method or object's property that's a closure
$obj->property();
, you have to do something like $obj->property->__invoke();
. Sometimes there isn't enough typing information to determine whether a method invocation is a closure on a property or a method on an object. Therefore, Pratphall has to emit something like (method_exists($obj, 'property') ? $obj->property() : $obj->property->__invoke());
. In order to avoid this, make sure your types are strong so the engine can tell which is which.
002: Pratphall doesn't support naming properties/variables and methods/functions the same, making declarations for external PHP w/ name ambiguities difficult
003: the JS arguments variable that gets the function arguments is unavailable if redefined later in function
004: String.charCodeAt only uses ord when emitting
005: String.indexOf emits more than just strpos.
006: Regular expressions are flat out unsupported
007: String.replace doesn't support callback
008: String.slice and String.substring are unsupported
009: String.toLocaleLowerCase and toLocaleUpperCase are unsupported
010: Object.valueOf is unsupported
011: Many Object functions (e.g. freeze) are unsupported
012: Array.reverse is unsupported
013: Array iteration functions don't really support second parameter "this"
014: Array.every is unsupported
015: Array.indexOf is unsupported
016: Array.join assumes comma by default
017: Array.lastIndexOf is unsupported
018: Array.reduceRight is unsupported
019: Second parameter of Array.slice is unsupported
020: Array.some is unsupported
021: Second parameter of JSON.parse is unsupported
022: Second and third parameters of JSON.stringify are unsupported
023: Class/interface names cannot be the same as module/namespace names at the same level
These are some wishlist items that could be implemented in the future.