DEV Community

Cover image for PsySh - More than just REPL
James Seconde
James Seconde

Posted on

PsySh - More than just REPL

Most backend developers know about interactive modes, that certainly isn’t a secret.

jimseconde@jim-amd-u18$: php -a

jimseconde@jim-amd-u18$: node

jimseconde@jim-amd-u18$: python3

All give you an active interpreter for that respective language. It’s used mostly for testing or confirming behaviour on the fly - ironically because you have a mountain of code in the middle of your app, your linter isn’t picking up on anything suspicious and exceptions are being thrown where you don’t want. Perhaps you want to look at how something is type juggling instead - again, your linter can’t help you with that.

PHP
jimseconde@jim-amd-u18$: php -a
> $foo = '342';
> $bar = 'give me a string';
> var_dump($foo + $bar);
Enter fullscreen mode Exit fullscreen mode
Node
jimseconde@jim-amd-u18$: node
> console.log(234 / 3)
Enter fullscreen mode Exit fullscreen mode
Python3
jimseconde@jim-amd-u18$: python3
> print('give me a string' . 3424)
Enter fullscreen mode Exit fullscreen mode

While an active interpreter is useful, it has one major limitation: it’s the standard
library of functions of a language, not a framework or your whole application's code.
So we’d have to import anything we want to test line by line:

Node
jimseconde@jim-amd-u18$: node
> barkNoise = 'woof'
> console.log(bark(barkNoise))
ReferenceError: bark is not defined
> import { bark } from './app/some_functions/myFunctions.js'
> console.log(bark(barkNoise))
woof
Enter fullscreen mode Exit fullscreen mode

PHP: How PSR-4 & Composer changed how we code in PHP

While it looks like a daunting name, PSR-4 is just the specification for how code is structured,
allowing a modular component system in your php app. At runtime, composer uses an autoloader
system to get every class you’ve got in your app and loads it into that php thread in memory.
This is why when you code something in, for example, Slim, Laravel or Symfony, you can use an
import to get a class from -somewhere else- in your app, and because of autoloading, it’s
available during runtime.

From PHP interactive shell to PsySh

We’ve already established that php -a just spins up a raw shell. But - what if we
could spin up a shell that uses composer’s autoloader to throw the whole app into memory,
then gives you your command line? If you did that, every single class and all the
functionality of your app sudden becomes available!

This is exactly what PsySh does. In Symfony or Slim, you can get it using composer:
composer require psy/psysh. In Laravel, a version of it comes out of the box, named tinker.
You can fire it up using php artisan tinker.

It's also worth noting that in tinker, not only has composer autoloaded every class, it's also fired Laravel's entry point to the app, including all of the bootstrapped features - so, features like the Service Container and Config are available:

jimseconde@jim-amd-u18$: php artisan tinker
>>> config('app.my_awesome_variable');
>>> $myPusherService = app()->make('PusherService');
>>> $myPusherService->pushToQueue(['send me']);
Enter fullscreen mode Exit fullscreen mode

More than debugging: 4 use cases

PsySh is usually described as a debugger, but I’d say it’s a lot more powerful than that. Let’s
have a look at four areas it can be used to interact with our apps:

Shopify API library

In previous blog posts, I've written how to use composer to get a REST API wrapper..
The wrapper is used to fetch and push whatever your app requires
in its business logic to interact with a Shopify store (in this case). It’s baked into your app’s code though,
so using something like Postman or Insomnia to manually query the API would be better suited for
quick queries. With PsySh though, the API is now -just available-. Even better, you may
have extended the API wrapper with bespoke functionality - now that’s available too. For example:

jimseconde@jim-amd-u18$: php artisan tinker
>>> $api = new \EastsideCo\Shopify();
>>> $shopifyProduct = $api->get('admin/products/324235.json')->products;
>>> $localProduct = new \App\Product::create($shopifyProduct);
>>> $localProduct->save();
Enter fullscreen mode Exit fullscreen mode

You'll notice that PsySh also gives you interactive feedback. If you call the Eloquent save() function for instance,
as long as the entity passes validation, it will return and print true below. In the case of an API response or the
result of a database query, it'll return and print a json array of the response, even if it's been set to a variable.

Your API class instance is generated the same way it would have been if
you were running the whole application at runtime (because, as everything has been
loaded in PsySh, technically this is exactly what has happened). So, whatever custom
methods you’ve coded are now available - for example:

>>> $blogPayload = ['09012938','My new blog post'];
>>> \App\API::createBlogPost($blogPayload);
true
>>> $api->fetchImagesByVariantId('90812309')
Enter fullscreen mode Exit fullscreen mode
Laravel’s Eloquent ORM interaction

The main usage you’ll see of Laravel’s inclusion of PsySh is in the ORM documentation, and
it’s a good example of where it’s extremely powerful. With
the ORM loaded completely into your command line, you can use any extended
custom functionality you’ve built into your ORM and test it.
This now effectively completely removes the need to access
your database’s command line - you can even write raw SQL
into the ORM and run that.

Examples:

jimseconde@jim-amd-u18$: php artisan tinker
>>> \DB::select(SELECT * FROM products WHERE id = 4)->get();
>>> \App\Product::find(342)
>>> \App\Variants::where('sku', 'like', 'TABLE1')->get();
Enter fullscreen mode Exit fullscreen mode
Laravel’s Storage driver

Here’s another example of where PsySh can be really powerful.
Every Laravel class that comes with the Framework is now available - so, let’s
take the File Storage system that is built on top of the powerful Flysystem PHP library.
Let’s imagine you need to extract a specific SQL query for data analysis.

>>> $dataQuery = \DB::('SELECT a.id, a.title, b.title, b.price, b.sku from products a INNER JOIN variants b on a.id = b.product_id')->get();
>>> // $dataQuery is a select query returned as a .json
>>> // dump out the data to the storage path
>>> \Storage::disk('local')->put('datadump.json', $dataQuery);
Enter fullscreen mode Exit fullscreen mode

On your server, your query result json is now written to /storage/app
(if you’ve still using the defaults, otherwise it will be written to the
configured path for the local driver).

We’ve just the local driver here, but if you want to you can use any of
the other drivers - such as the AWS S3 one. Want to send the same
payload into an S3 bucket? Sure!

>>> \Storage::disk('s3')->put('datadump.json', $dataQuery);
Enter fullscreen mode Exit fullscreen mode
Dispatching Jobs or Events

If your app makes extensive use of the Job queue system (currently
I use AWS’s SQS messaging system for Job queues, and use Redis for broadcasting
events over websockets) or has a more a complex Event-driven architecture system,
you can use PsySh to do routine maintenance or to debug the application structure.
Here’s an example of fetching an order by the API, then firing an event to the queue listeners:

>>> $orderToCancel = $api->get('admin/orders/32448.json');
>>> // fire event to the queue to cancel the job
>>> Event::dispatch(new CancelOrderJob($orderToCancel));
Enter fullscreen mode Exit fullscreen mode

I've found the power of an interactive shell like this to be invaluable in day-to-day operations. For more
information on PsySh and tinker, check out the docs and repos:

https://psysh.org/

https://github.com/bobthecow/psysh

https://github.com/laravel/tinker

To see an example of the same kind of enhanced REPL power outside of PHP, I recommend having a look at how Python's Django implements the same patterns using ./manage.py shell_plus --ipython.

Happy shelling!

Top comments (1)

Collapse
 
shalvah profile image
Shalvah

Great article! I use Tinker all the time to run one-off jobs or check something that's not working right. I even have Psysh installed globally as my default shell.