Mug alert as a Symfony command

12 december 2020
Yeah I am lazy sometimes, I do automate things a lot. This was a fun dev for my love using Symfony components.

Symfony Slack

Tell me a story

My love ❤️ adores mugs. I mean a lot. She recently saw one on Shopify, a special limited edition for Christmas from a french creator. It was planed to be released small quantity by small quantity across the month of december.

She really wanted this mug. So a week ago, she asked me if "I can do something" so that we can be alerted when some stock are online before it goes out of stock.

"Of course I can". I mean, it is part of my job/hobby/passion to develop things :)
I use Symfony since +10 years for personal and professional projects, and obviously I come to a decision to fastly create a Symfony command for this. It will run as a cronjob on a small server I have for this personal project.

Proof of Concept in 10 minutes ⌚️

The Shopify page is "just" a piece of HTML returned from the server. And some JavaScript. I had to get it first, then check the right element inside of it and detect if there is stock or not, to finaly be instantly alerted.

Tools used:
  • Console as the entry point/glue code of the script
  • MakerBundle because, ho I told you already, I am lazy
  • HttpClient to request the endpoint and get the raw content HTML back
  • DomCrawler to filter this HTML and find the proper element
  • Notifier and an integration which I crontributed to were used for the SMS alert
  • Mailer to send an email as well
  • and my week resting brain 🤷🏻‍♂️
Show me some code
The HttpClient is as simple as requesting with the proper HTTP method the desired url. I got back an HTML and a status code.

	// Symfony\Contracts\HttpClient\HttpClientInterface $httpClient in __construct()
	// $this->httpClient = $httpClient;

	$response = $this->httpClient->request(
	statusCode = $response->getStatusCode();
	$htmlContent = $response->getContent();
But the button add to cart is handled by JavaScript code, and the client can not handle it. I found then another HTML element I could rely on with the DomCrawler. I opened two pages, a sold out and an online product to compare this element and its text on both pages. The text was indeed different when "sold out" or "purchasable" 👍🏻.

	// Symfony\Component\DomCrawler\Crawler in the code

	$crawler = new Crawler($htmlContent);
	$selector = $crawler->filter('#html-page-element');
	$selectorText = $selector->text();
Finaly it is just a check on the value, and use the Notifier and its 3 lines configuration to get the alert SMS.

	// Symfony\Contracts\HttpClient\HttpClientInterface $texter in __construct()
	// Symfony\Component\Notifier\Message\SmsMessage in the code
	// $this->texter = $texter;

	$this->texter->send(new SmsMessage('0102030405', 'My Message'));

And voilà!

Fun fact (for me) / Sad fact (for her): I got an alert on monday morning for stock online. But I was driving. And you know, no phone while driving.
Some time after, she sat next to me because she saw on Instagram some people that had the chance to get their mug this morning. This partial stock was online for ~10 minutes (I got 2 alerts SMS, cron was configured for every 5 m).

More alert 🔔💌

She was sad all monday long. For her the script was not working properly. Of course it worked as expected :)
Unfortunatly, the FreeMobile SMS feature (and its Notifier integration) is by design only allowed to send SMS to your number. I conclude she will received a flat email alert as well :)

Victory ✌🏻

SMS Alert

We both received the alert thursday morning, SMS for me and mail for her.
We ordered it directly and she was so joyful after! I felt like 🎅🏻 but more like a 👨🏻‍💻🎅🏻.

Mug order

After all, that is my job. Having fun resolving problems with tools that do the job, clear, fast and simple. While trying to make people happy.

On Symfony Slack where I posted for fun this fact, people wrote me they wanted the full story.
So this was the mug alert story.
Thank you for reading, I hope you liked it. Have a great day :)

Full code (adapted)

Here is the full code, adapted and with explanations:

website | @amakdessi | contact | © 2021 Antoine Makdessi