PHP and REPL

Having a Read-Eval-Print-Loop changes the way you code. You can isolate a relatively small piece of code and run it until you've got what you need. It provides a live documentation for whatever you are doing. It makes your mind less cluttered and your programming language experience better.

Can't remember how Ruby DateTime#parse works?

Run irb or rails c if you have Rails. Check whatever you like in your working env.

2.3.0 :001 > require 'date'
 => true 
2.3.0 :002 > DateTime.parse('3rd Feb 2001 04:05:06+03:30')
 => #<DateTime: 2001-02-03T04:05:06+03:30 ((2451944j,2106s,0n),+12600s,2299161j)> 

Need some quick'n dirty Python API scrapping?

No problem. Just run python. Most frameworks also have their own console.

>>> import requests
>>> r = requests.get('https://api.github.com/user', auth=('user', 'pass'))
>>> r.json()
{u'private_gists': 419, u'total_private_repos': 77, ...}

Need to check what is {}+{} in JavaScript?

.
Not what it used to be.

Apparently not what it used to be. Nevertheless, the console is pure magic.

Want to check a Symfony2 DQL query?

Uh...There is a command to do this.

$ bin/console doctrine:query:dql 'SELECT b FROM GameBundle:Badge b'
array(26) {
  [0]=>
  object(stdClass)#1735 (21) {
    ["__CLASS__"]=>
    string(30) "GameBundle\Entity\Badge"
    ["id"]=>
    int(27)
    ["name"]=>
...

It's not very useful. Formatting is difficult to read and I can't put this result into a service to process it further.

Most developers will run it on a web server and dump results into Symfony2 debug toolbar. Some of them will output var_dump into the page itself.

Maybe it's just Symfony2

Let's try something simpler, the difference between two dates. Let's run php -a.

php > var_dump((new \DateTime('now'))->diff(new \DateTime('yesterday')));
object(DateInterval)#4 (15) {
  ["y"]=>
  int(0)
  ["m"]=>
  int(0)
  ["d"]=>
  int(1)
... 

I've spend some time writing this because there is no [tab][tab] completion.There is [tab][tab] completion, but it doesn't work for method names. Not to mention you have to end your line with a semicolon and you have to put it through var_dump. Horrifying.

No wonder I haven't seen any junior to mid-level PHP coders using REPLs.

PsySH to the rescue

PsySH is a PHP shell with auto-completion, namespace support, docs support, reflection support, exception catching and readable formatting.

>>> new \Date[tab][tab]
DateInterval      DatePeriod        DateTime          DateTimeImmutable DateTimeZone     
>>> new \Date

It works for method names. The result is readable and useful. It has a working command history (ctrl+r). It will change the way you write code in PHP.

Much easier on eyes

You can also use it as a debugger by pasting in your code

eval(\Psy\sh());

This is difficult to type compared to Ruby binding.pry or JavaScript debugger;, but it's better than nothing.

Since PsySH is extendable we can also improve our workflow with Symfony2.

Just drop in this bundle in your dev environment. You'll have access to bin/console psysh command which will launch PsySH with the following variables already set:

  • $container - an instance of Symfony ServiceContainer, get all your services. Tip: declare Doctrine repositories as services
  • $kernel - an instance of Symfony Kernel
  • $parameters - an instance of Symfony parameters
Update

If you want to expose any configuration variable or service in the default variables for PsyshBundle you can set them easily in the bundle configuration (remember to add it to config_dev.yml, not config.yml since it should be only available in dev environment).

The default list is mentioned here with the rest of the config:

# app/config/config_dev.yml

services:
    psysh.shell:
        # Note: you can extend Shell class
        class: Psy\Shell
        # Or do some other initialisation (some debug output for newly launched console?)
        calls:
            - method: setScopeVariables
              arguments:
                -
                    kernel: @kernel
                    container: @service_container
                    parameters: @=service('service_container').getParameterBag().all()
                    otherService: @my_service
                    # if typing $container is too much
                    c: @service_container