Categories
Software Development Web Development

Symfony 5+ how to view dump output with AJAX requests

So the idea is to be able to use the Symfony VarDumper component to be able to dump values when sending an AJAX request and view them for debugging while in development mode, but not in production mode.

Create a new directory

To do this you will need to create an event subscriber and listen for the kernel.response event. To do this you create a new directory inside your root directory, I called mine EventSubscribers, the name doesn’t matter you could call yous BlueWaffle and it will work exactly the same.

Create a new file

Inside this new directory create a new subscriber class, I called mine AjaxDumpSubscriber. You can make your subscriber implement

 implements EventSubscriberInterface

I am not sure if that is needed, I saw it somewhere in the docs. They never mentioned if it was needed so I am assuming it is maybe needed.

Then add the method stub for

getSubscribedEvents()

Here is what the github source code comment says about this method

interface EventSubscriberInterface
{
    /**
     * Returns an array of event names this subscriber wants to listen to.
     *
     * The array keys are event names and the value can be:
     *
     *  * The method name to call (priority defaults to 0)
     *  * An array composed of the method name to call and the priority
     *  * An array of arrays composed of the method names to call and respective
     *    priorities, or 0 if unset
     *
     * For instance:
     *
     *  * ['eventName' => 'methodName']
     *  * ['eventName' => ['methodName', $priority]]
     *  * ['eventName' => [['methodName1', $priority], ['methodName2']]]
     *
     * The code must not depend on runtime state as it will only be called at compile time.
     * All logic depending on runtime state must be put into the individual methods handling the events.
     *
     * @return array<string, string|array{0: string, 1: int}|list<array{0: string, 1?: int}>>
     */
    public static function getSubscribedEvents();
}

The full class

The entire class should look like this, from what I can tell. This is working code.

namespace App\EventSubscriber;

use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpKernel\Event\ResponseEvent;
use Symfony\Component\HttpKernel\KernelEvents;

class AjaxDumpSubscriber  implements EventSubscriberInterface
{

    /**
     * @return array
     */
    public static function getSubscribedEvents(): array
    {
        return [KernelEvents::RESPONSE];
    }

    public function onKernelResponse(ResponseEvent $event)
    {
        if (!$event->getKernel()->isDebug()) {
            return;
        }
        $request = $event->getRequest();

        if (!$request->isXmlHttpRequest()) {
            return;
        }
        $response = $event->getResponse();
        //set this one value only if this is in development mode
        $response->headers->set('Symfony-Debug-Toolbar-Replace', 1);
    }
}

That is it, now you can dump() values like normal… but you can’t view them like normal, not in Symfony 5.4, not for me anyways .

This only partially works, maybe I missed something or I expect something? Most of the time when you use dump() there will be an icon in the profiler bar that indicates debug output was output aka. dump() was used. Usually you can click on it and quickly see the values that were dumped. This feature doesn’t work with  AJAX requests at the moment, from what I can observe.

Instead you need to click the response when it indicates in the profiler then click the Debug tab on the left panel to see your dump() values.

Links

The VarDumper component. -> Docs link you must install this before you can dump anything.

The profiler component -> Docs link you need this to see dumped stuff, even though it doesn’t work properly with the VarDumper

Creating an EventSubscriber -> Docs link

Using event subscribers -> docs link

 

Categories
Software Development Web Development

How to update Symfony minor versions

This is a work in progress. I’ll keep updating it with time.  Most of my articles are for me so I can remember what I did and the problems I had and how I solved them.

I’d suggest updating your composer packages more often than just when you are moving to a newer version of Symfony. That alone is the most time consuming part. If you keep your packages updated it is less work when you need to update to minor/major new versions.

Read the documentation about this subject first.  This is a walk through or an overview with a little more information than the docs, along with some jokes.

First you need to  know some things about composer, so  read the docs, watch some videos, do some googling, learn more and refresh your brain.

Next you’ll need to be fairly knowledgeable of .git and it’s many commands and how it functions. Read that link to the short little book that quickly explains most of it, you only really need like the first 3 -4 chapters fresh in your mind.

yoda practice meme
Yes you must.

Unless your app is tiny an simple, I suggest you practice updating your app first and take notes so that you can have a smoother flow when you update your actual project. For example if you try to update your composer packages all to the latest, you will find conflicts and other issues, some will need to be downgrade some etc.

For me it is a lot less stressful to clone my app in a different directory and quickly try the process and take notes and make sure not to push back to the main repo… unless I just get lucky and shit magically works 100%. I messed up a lot during this process the first time. I had not fully updated my app and some things were left all the way from version like 3.4?

It depends on how old your app is aka Symfony version. How well it has been kept updated and much more, like what bundles you use etc.. This can take many hours depending on the size of your project, how much you know about git, how much you know about composer and how good your google skills are.

Doing Research!!!

You will want to check the change log. This Symfony article mentions how to find that information in the changelog. This symfonycast article has some more good information about Symfony recipes.

Most of all look closely at the output and read what it tells you. I’d recommend you spend a day or two messing around unless your app is tiny.

If you are upgrading from 5.* to 5.4 then here is a long list of things you need to consider. change log

You can either create a new directory and pull your project and play around first or you can also branch your code.   You will need to commit every time you run composer recipes:update or it won’t let you run it again. This means you could be making lots of commits and later diffing and merging and resolving conflicts. This is why I said you will need to be fairly good with GIT, just read the first few chapters of the book.

You might want to refresh your mind of the Symfony configurations because some of it will be changes to configs, some have changed and some may be different than what your current version uses. Make sure you choose the right version of Symfony to match yours in the left top corner when reading the docs.

symfony docs version example
Choose the version that matches the one you are upgrading to.

Step 1ish Updating composer.json

Here is my composer.json for example

{
    "type": "project",
    "name": "sogi/sogi",
    "description": "A total waste of my time",
    "license": "proprietary",
    "require": {
        "php": "^8.1",
        "ext-ctype": "*",
        "ext-fileinfo": "*",
        "ext-gd": "*",
        "ext-iconv": "*",
        "ext-json": "*",
        "ext-memcached": "*",
        "ext-pdo": "*",
        "ext-redis": "*",
        "aws/aws-sdk-php": "^3.209",
        "cocur/slugify": "^4.0",
        "composer/package-versions-deprecated": "^1.10",
        "doctrine/annotations": "^1.13.2",
        "doctrine/common": "^3.2",
        "doctrine/doctrine-bundle": "^2.5.5",
        "doctrine/doctrine-migrations-bundle": "^3.2",
        "doctrine/orm": "^2.11.1",
        "friendsofsymfony/jsrouting-bundle": "^2.7",
        "phpdocumentor/reflection-docblock": "^5.2",
        "ramsey/uuid": "^4.2",
        "ramsey/uuid-doctrine": "^1.7",
        "sensio/framework-extra-bundle": "^5.1",
        "symfony/asset": "5.4.*",
        "symfony/config": "5.4.*",
        "symfony/console": "5.4.*",
        "symfony/dotenv": "5.4.*",
        "symfony/event-dispatcher": "5.4.*",
        "symfony/expression-language": "5.4.*",
        "symfony/flex": "^1.3.1",
        "symfony/form": "5.4.*",
        "symfony/framework-bundle": "5.4.*",
        "symfony/http-client": "5.4.*",
        "symfony/intl": "5.4.*",
        "symfony/mailer": "5.4.*",
        "symfony/messenger": "5.4.*",
        "symfony/mime": "5.4.*",
        "symfony/monolog-bundle": "^3.1",
        "symfony/notifier": "5.4.*",
        "symfony/process": "5.4.*",
        "symfony/property-access": "5.4.*",
        "symfony/property-info": "5.4.*",
        "symfony/proxy-manager-bridge": "5.4.*",
        "symfony/runtime": "5.4.*",
        "symfony/security-bundle": "5.4.*",
        "symfony/security-csrf": "5.4.*",
        "symfony/security-http": "5.4.*",
        "symfony/serializer": "5.4.*",
        "symfony/string": "5.4.*",
        "symfony/translation": "5.4.*",
        "symfony/translation-contracts": "2.3.*",
        "symfony/twig-bundle": "5.4.*",
        "symfony/validator": "5.4.*",
        "symfony/web-link": "5.4.*",
        "symfony/webpack-encore-bundle": "^1.13.2",
        "symfony/yaml": "5.4.*",
        "symfonycasts/reset-password-bundle": "^1.1",
        "twig/extra-bundle": "3.2.1",
        "twig/twig": "3.2.1"
    },
    "require-dev": {
        "dama/doctrine-test-bundle": "^6.3",
        "doctrine/doctrine-fixtures-bundle": "^3.4.1",
        "symfony/browser-kit": "5.4.*",
        "symfony/css-selector": "5.2.*",
        "symfony/debug-bundle": "^5.2",
        "symfony/maker-bundle": "^v1.28.0",
        "symfony/phpunit-bridge": "^5.2",
        "symfony/stopwatch": "^5.2",
        "symfony/var-dumper": "5.2.*",
        "symfony/web-profiler-bundle": "^5.2"
    },
    "config": {
        "optimize-autoloader": true,
        "preferred-install": {
            "*": "dist"
        },
        "sort-packages": true,
        "allow-plugins": {
            "symfony/flex": true,
            "symfony/runtime": true
        }
    },
    "autoload": {
        "psr-4": {
            "App\\": "src/"
        }
    },
    "autoload-dev": {
        "psr-4": {
            "App\\Tests\\": "tests/"
        }
    },
    "replace": {
        "paragonie/random_compat": "2.*",
        "symfony/polyfill-ctype": "*",
        "symfony/polyfill-iconv": "*",
        "symfony/polyfill-php72": "*",
        "symfony/polyfill-php71": "*",
        "symfony/polyfill-php70": "*",
        "symfony/polyfill-php56": "*"
    },
    "scripts": {
        "auto-scripts": {
            "cache:clear": "symfony-cmd",
            "assets:install %PUBLIC_DIR%": "symfony-cmd"
        },
        "post-install-cmd": [
            "@auto-scripts"
        ],
        "post-update-cmd": [
            "@auto-scripts"
        ]
    },
    "conflict": {
        "symfony/symfony": "*"
    },
    "extra": {
        "symfony": {
            "allow-contrib": true,
            "require": "5.4.*"
        }
    }
}

Each line you see above in the require sections, may need to be updated.

cat that is a lot meme
You could have more lines than this depending on your project size and complexity

This to me is one of the most complicated and time consuming parts. This is where the printed composer.json file comes in handy to let you see what all you are using and take notes next to each one when you  go to packagist to look them up and find the latest versions of each. One problem you will find when updating is conflicts, you will need to downgrade some things in order to work with others. So you make a change to the latest package and then later another bundle you use needs an older version and so composer downgrades it.

This is only the first part of the battle.

gi joe half the battle meme
Now you know!!!

Here is the output I got after updating the package/bundle versions and using composer update, after running composer recipes:update. Basically I ran the composer update command after running the composer recipes:update command, a little out of order.

You can see some things were downgraded.

Below is the packagist page for twig, notice it is v3.3.8 that is the number you need if you want to update to the latest version.

Packagist for twig

You need to do that for each package you want updated. For Symfony it is mostly easy, as you can change minor versions from like 5.1 to 5.4 and run composer update. For others you need to write the version down and add it to composer and then run update, read the output and follow it.

I also suggest running composer update after every change. Don’t make the mistake I once made and hop right along and change a bunch then try to update. OH HELL COMES UPON YOU IF YOU DO THAT.  You will have mistakes, conflicts to resolve etc. It is much easier to make one change, update, fix/change and move to the next.

For many of these composer packages you will need to read the output carefully and possible update dependencies. That is one of the main reasons I say go slow one at a time.

The Log of Changes

holy change log batman meme
Read the changes carefully

You will need to view the change log changes and hunt for possible places in your code where code changes need to be made. Like I said from just version 5.3 to 5.4 there is a lot. You may have to dig even further back if your version is older like 5.1 or 5.2ish etc. That is what I’ve been doing, some of my bundles were ancient.

This part is sort of painful and slow too. What I did was check the deprecation’s listed in app profiler bar, make one change check it again… What I found to help was to use my IDE PHPStorm’s find and replace feature to find all the places where object xxx was used and replace it with yyy. Some of the notices might make you say WTF and scratch your head.

Uhm, wait… what?

And you must google and search and figure it out. It was not fun. It required many curse words.

Composer Recipes

This is the next step.

This is something I’ve just heard of. I am figuring this  out currently and will update this section as I figure it out, ruin some shit, take screenshots etc. Here is the Symfony release article about this feature with more info.
But this so far is how I’ve destroyed my app(the local copy)

DO NOT RUN THE WEBPACK ENCORE UPDATE, it destroys your F***ing encore configuration by replacing it.

Remember these updates are not interactive. They don’t show you what they are going to do ahead of time and ask you if it is ok.  They don’t ask you if you’d like to do xyz. They just do shit, tell you there was a conflict and BOOM it is up to you to find the files and fix them. You use git status or other commands to see what was changed, where etc. Then you open the file and delete the parts you don’t want. Maybe there is a better way, but this is how I did it. The ours>>> === theirs >>> didn’t make much sense to me, it didn’t seem to follow the way it should work in several files, some were better than others.  It was hard to see what changes happened in the file. The longer the file, the harder it was to discern the changes. Like I said I WRECKED some shit the first few times.

The only way I can figure to update Webpack is to rename the file that gets destroyed then copy/cut/replace with my working code again. This is the reason I said to practices before you do a ANYTHING. The first time around you are bound to WRECK YOUR APP. My first time I lost all kinds of configuration etc. I totally wrecked my app locally and had to delete the whole damn folder and clone my repo and start again. The second time I did a little better and that is when I decided to write this article. The third time as the charm… except for Webpack.

Here is a short video demonstrating some of the coolness of composer recipes:update, it works pretty good, but you got to play with it and learn WTF is going on first. LOL

 

I need to capture one with conflicts and show that next. I’ll pull from an older commit later and redo the process and capture more. I screwed up  the version in the video by blindly updating Webpack, it fubared my precious config.

Categories
Software Development Web Development

How to set application values for easy reuse in Symfony 5+

This article is a work in progress and will be further updated much more in the future. I don’t feel it is complete yet, I’d like to cover more details, more screenshots etc..

So how do you set/configure a value like an upload directory in one location in Symfony so that you can easily use it in your app later? It is actually easier than it even sounds.

Symfony has this handy system you can use to set parameters once and fetch them later. Here in this SymfonyCast under the section about moving an uploaded file, you will see this line.

$destination = $this->getParameter('kernel.project_dir').'/public/uploads';

Forget a moment about the full string. $this->getParameter() is what I want to focus on. The Symfony documentation metions accessing configuration parameters. And that $this->getParameters() is how you get to them.

The docs go further and say this is how you access them in the controller. To get them in your services you should autowire them and if you need them in services often you should bind them. I’ll add that below.

If you mostly need parameters in controllers, setting them as parameters like below is easy. If you need them in multiple services, then binding them is easier.

You can set your own parameters in the file /app/config/services.yaml under a section named… “Parameters”  or “bind” them globally under the _defaults section.

Mostly controllers

Lets look at how to do that really quickly. Here is what is in the top portion of my file.

# /app/config/services.yaml
# This file is the entry point to configure your own services.
# Files in the packages/ subdirectory configure your dependencies.

# Put parameters here that don't need to change on each machine where the app is deployed
# https://symfony.com/doc/current/best_practices/configuration.html#application-related-configuration
parameters:
  app.db_user: '%env(DATABASE_USER)%'
  app.db_pass: '%env(DATABASE_PASSWORD)%'
  app.user_uploads_dir: '%kernel.project_dir%/uploads'
services:
  # default configuration for services in *this* file
  _defaults:
    autowire: true      # Automatically injects dependencies in your services.
    autoconfigure: true # Automatically registers your services as commands, event subscribers, etc.

The whole file is really long I just wanted enough to show a small section so you could identify it visually. You can find more about the configuration parameters in the docs here.

See the parameters section. There are three parameters I have set. app.user_upload_dir: ‘%kernel.project_dir%/uploads’ is one I made up for my own use. With that syntax I don’t have to add anything to the end of it like in the SymfonyCast code above.

I think it shows this type change later in the SymfonyCast too. You should read that full SymfonyCast if you are interested in learning how to work with uploads in Symfony 5+ Here is another great SymfonyCast all about configuration in Symfony 5+ I highly suggest you read it.

But anyways like the docs show, if I need the value of the uploads directory inside a controller, I simply access it like this.

$uploadsDir = $this->getParameter('app.user_uploads_dir');

And I don’t have to worry about misspelling it, and it can be changed in one location now if it ever needs to be changed.

Here is another link from the Symfony documentation about configuration overall.

Using Bind

Using bind seems to work more easily in more places. I am not sure of any disadvantages, the main advantage seems to be ease of use. The other methods require you to autowire some other service and call some specific method. My memory sucks, so this is the easier way for me . LOL

The documentation on this subject isn’t great, it quickly glosses over the subject.

The best info I could find on bind and how it works is actually in a SymfonyCast like usual.

Below is what  I have set for example.

# default configuration for services in *this* file
_defaults:
  autowire: true      # Automatically injects dependencies in your services.
  autoconfigure: true # Automatically registers your services as commands, event subscribers, etc.
  bind:
    # autowire this variable name to get the value below
    $userUploads: '%kernel.project_dir%/uploads'

Now to use the value of $userUploads all I do is add it to the Controller route definition as a parameter or add it to the __construct() of a Service class aka any other class.

Example uses

For a Controller router method.

public function save_user_image(Request $request, Security $security,
                                MessageBusInterface $messageBus, $userUploads): JsonResponse

For a Service constructor.

public function __construct(LoggerInterface $exceptionsLogger, $userUploads)
    {
        $this->exceptionLogger = $exceptionsLogger;
        $this->userUploadsDir = $userUploads . '/';
    }

How do you view the parameters?

If you are using bind, you simply look at what you have defined in the services.yaml file.
To see what parameters you can access use the following command. Only values you have defined under the “parameters” section of the services.yaml file and the parameters that symfony sets internally are visible with this command.

php bin/console debug:container --parameters

That will output a really long list of all of the parameters and info about them like this.

symfony output parameters
A list of the Symfony parameters you can use

There are literally hundreds of them. So many I am willing to bet if you need to use something or know where it is, this will show you.

Categories
Software Development Web Development

How to fix Composer GitHub API limit (0 calls/hr) is exhausted … errors

Programming Joke.

Modern web development keeps getting easier by the day

How does a mechanic start their day? They go to work. They get their tools and start working.

How does a carpenter start their day? They go to work. They pick up their tools and begin where they left off the previous day.

How does a web developer start their day? They go to work. Turn on their computer.  Start up the needed programs. Discover updates are needed. Start to do the updates. Failures happen. Spend entire day finding every bug and quirk in every piece of software they use. Maybe their OS shit the bed. Maybe their frameworks need updating… So they spend 4 hours googling and fixing errors in the first tool. Get to the next tool, same thing, spend 2 hours fixing it. Get to the next tool and have to spend 4 hours googling, updating and fixing it. In the midst of all of that they wreck a few things and have to restart a few dozen times. Finally 10 hours later their tools are maybe ready for programming the next day.

These days as a web developer you spend more time fixing your tools than programming. Programming in Web languages is the absolute worst of the worst.

If web developers were mechanics we would be ordering new tools every day and waiting for them to arrive before we could start work because the previous days tools were either broken, stolen or worked entirely differently today than they did yesterday… and the manufacturers docs were not updated, so we have to just play with them until we figure out how they work now.

programmers tools are all like…

Now to fix the  Github issue with composer.

Side note: a long time ago I’d get these types of  errors when simply pushing my code to my own repository.

“gitHub API limit (0 calls/hr) is exhausted, could not fetch https://api.github.com/graphql. Create a GitHub OAuth token to go over the API rate limit. You can also wait until ? for the rate limit to reset.”

Aren’t we all just like..

When you get that error while trying to use composer…

Well the Composer docs say the following

Because of GitHub’s rate limits on their API it can happen that Composer prompts for authentication asking your username and password so it can go ahead with its work.

If you would prefer not to provide your GitHub credentials to Composer you can manually create a token using the procedure documented here.

Now Composer should install/update without asking for authentication.

I don’t know about you, but Composer has never once prompted me for any password. It sort of just started doing this.

Here is what I randomly got today while trying to update a Symfony project.

Calculating CHANGELOG…GitHub API limit (0 calls/hr) is exhausted, could not fetch https://api.github.com/search/issues?q=a1a70353f64f405cfbacfc4ce860af623442d6e5. Create a GitHub OAuth token to go over the API rate limit. You can also wait until ? for the rate limit to reset.

Composer never asked for any authentication password. I do notice this error it is finally prompting for a github token below this error in another message. I have no idea when this began, I used composer a few weeks ago it seems and this wasn’t an issue.

You can enter the token there and it says it saves it.  DON’T DO THAT. Stop now. Do not create and enter the token at this prompt or you will be dreadfully sorry, you will get nothing but errors about the malformed token if you do. DON’T DO IT. Hit ctrl+c to quit or close your terminal, but whatever you do, do not enter that token yet, until you have the latest version of Composer installed.

Find your version with

 
//globally installed
composer --verision

//or loally installed
php composer.phar --version

If your version composer is not the latest version then update it(2.2.9 as of this article) To do this, if you installed composer locally then you delete the composer.phar file in your project folder. If you installed it globally then you need to go to that directory probably in

/usr/local/bin/composer

where composer is the filename not a directory. Delete the composer file. You will see suggestions online that you can just run those commands and you don’t need to delete composer. That didn’t work for me. I only got permission denied errors instead. So I rage deleted the damn thing and issued the commands again and

El magico

wow magic meme
Wow like magic it works now

No more permission denied now you summonabich!!! LOL

Then rerun the commands to install composer either locally or globally. I prefer globally so I don’t have to screw around with updating each project.

Then you need to create this special token to use Composer.

You are special and need a special token!

How to create and use the token? That is explained in the Composer Docs here. You go to the github page seen below

create github token
Create github token

Don’t choose any of the Scopes, just choose to create this useless token, it doesn’t need them.

You will next see this screen.

Now you copy that token. Open your terminal and use the following command with your token where the word token is.

//locally installed composer
php composer.phar config github-oauth.github.com token
//globally installed like I suggested
composer config --global github-oauth.github.com token

And now composer should be working again.

boom how it is done baby meme
Trial and error until it works

 

Categories
Software Development Web Development

Symfony 5+ error logging handlers explained

This will be a super long article, this subject is way more complex than it originally sounds. As always, I’ll be updating this soon with more info and possibly more examples as I figure it out better.

First off, you are not limited to just one logger in Symfony. However, the most popular is Monolog and Symfony has built in support making it easier to implement. So that is what I cover here.

One thing you might want to use a logger for is logging specific exceptions or errors to specific locations. You may also just want to log some value in your code for debugging purposes to see what is happening or being sent to a particular method/function. These are good use cases for logging.

While most errors/exceptions already display with lots of info in the browser, you may be like me and want certain things logged so you can review the entire pattern over a period of time.

The steps required to log sound sort of simple:

  1. Install the logger
  2. Configure the logger, decide how you want it to actually work. You don’t have to configure it with yaml. Doing so makes it so that you can autowire loggers instead of having to create a new object passing values in each time which will usually be the same values.
  3. Get the logger into your class some how( this can vary depending on whether it is a controller or a service/class)
  4. Use the logger to log something like an exception.

Installing

Your project might already have monolog logger installed since Symfony uses it by default to log everything. It doesn’t matter. Running the line below won’t hurt even if it is installed already.

To install the Monolog Logger you simply open your terminal, navigate to the root of your project and type the following :

composer require symfony/monolog-bundle

That is all. Now the monolog bundle is installed and you can use it after you configure it. And that is where the fun and confusion begin. Symfony uses Monolog so it may already be installed  depending on how you created your project.

Handlers

First decide what types of loggers you want to use. You will probably see people using the stream type logger most often. This is really generic and is the default. I don’t like the stream handler because it just keeps growing infinitely.

I prefer storing different logs to different places. You can even store the logs in a database if you want. The good news is you can use more than one type of logger, and configure them however you want.

First off, here is a long list of handler types you can use with Monolog. This is going to be SUPER LONG. I’d rather just list it here than link to a file. This list is taken from the code on github

* Possible handler types and related configurations (brackets indicate optional params):
 *
 * - service:
 *   - id
 *
 * - stream:
 *   - path: string
 *   - [level]: level name or int value, defaults to DEBUG
 *   - [bubble]: bool, defaults to true
 *   - [file_permission]: int|null, defaults to null (0644)
 *   - [use_locking]: bool, defaults to false
 *
 * - console:
 *   - [verbosity_levels]: level => verbosity configuration
 *   - [level]: level name or int value, defaults to DEBUG
 *   - [bubble]: bool, defaults to true
 *   - [console_formatter_options]: array
 *
 * - firephp:
 *   - [level]: level name or int value, defaults to DEBUG
 *   - [bubble]: bool, defaults to true
 *
 * - browser_console:
 *   - [level]: level name or int value, defaults to DEBUG
 *   - [bubble]: bool, defaults to true
 *
 * - gelf:
 *   - publisher: {id: ...} or {hostname: ..., port: ..., chunk_size: ...}
 *   - [level]: level name or int value, defaults to DEBUG
 *   - [bubble]: bool, defaults to true
 *
 * - chromephp:
 *   - [level]: level name or int value, defaults to DEBUG
 *   - [bubble]: bool, defaults to true
 *
 * - rotating_file:
 *   - path: string
 *   - [max_files]: files to keep, defaults to zero (infinite)
 *   - [level]: level name or int value, defaults to DEBUG
 *   - [bubble]: bool, defaults to true
 *   - [file_permission]: string|null, defaults to null
 *   - [use_locking]: bool, defaults to false
 *   - [filename_format]: string, defaults to '{filename}-{date}'
 *   - [date_format]: string, defaults to 'Y-m-d'
 *
 * - mongo:
 *   - mongo:
 *      - id: optional if host is given
 *      - host: database host name, optional if id is given
 *      - [port]: defaults to 27017
 *      - [user]: database user name
 *      - pass: mandatory only if user is present
 *      - [database]: defaults to monolog
 *      - [collection]: defaults to logs
 *   - [level]: level name or int value, defaults to DEBUG
 *   - [bubble]: bool, defaults to true
 *
 * - elasticsearch:
 *   - elasticsearch:
 *      - id: optional if host is given
 *      - host: elastic search host name. Do not prepend with http(s)://
 *      - [port]: defaults to 9200
 *      - [transport]: transport protocol (http by default)
 *      - [user]: elastic search user name
 *      - [password]: elastic search user password
 *   - [index]: index name, defaults to monolog
 *   - [document_type]: document_type, defaults to logs
 *   - [level]: level name or int value, defaults to DEBUG
 *   - [bubble]: bool, defaults to true
 *
 * - redis:
 *   - redis:
 *      - id: optional if host is given
 *      - host: 127.0.0.1
 *      - password: null
 *      - port: 6379
 *      - database: 0
 *      - key_name: monolog_redis
 *
 * - predis:
 *   - redis:
 *      - id: optional if host is given
 *      - host: tcp://10.0.0.1:6379
 *      - key_name: monolog_redis
 *
 * - fingers_crossed:
 *   - handler: the wrapped handler's name
 *   - [action_level|activation_strategy]: minimum level or service id to activate the handler, defaults to WARNING
 *   - [excluded_404s]: if set, the strategy will be changed to one that excludes 404s coming from URLs matching any of those patterns
 *   - [excluded_http_codes]: if set, the strategy will be changed to one that excludes specific HTTP codes (requires Symfony Monolog bridge 4.1+)
 *   - [buffer_size]: defaults to 0 (unlimited)
 *   - [stop_buffering]: bool to disable buffering once the handler has been activated, defaults to true
 *   - [passthru_level]: level name or int value for messages to always flush, disabled by default
 *   - [bubble]: bool, defaults to true
 *
 * - filter:
 *   - handler: the wrapped handler's name
 *   - [accepted_levels]: list of levels to accept
 *   - [min_level]: minimum level to accept (only used if accepted_levels not specified)
 *   - [max_level]: maximum level to accept (only used if accepted_levels not specified)
 *   - [bubble]: bool, defaults to true
 *
 * - buffer:
 *   - handler: the wrapped handler's name
 *   - [buffer_size]: defaults to 0 (unlimited)
 *   - [level]: level name or int value, defaults to DEBUG
 *   - [bubble]: bool, defaults to true
 *   - [flush_on_overflow]: bool, defaults to false
 *
 * - deduplication:
 *   - handler: the wrapped handler's name
 *   - [store]: The file/path where the deduplication log should be kept, defaults to %kernel.cache_dir%/monolog_dedup_*
 *   - [deduplication_level]: The minimum logging level for log records to be looked at for deduplication purposes, defaults to ERROR
 *   - [time]: The period (in seconds) during which duplicate entries should be suppressed after a given log is sent through, defaults to 60
 *   - [bubble]: bool, defaults to true
 *
 * - group:
 *   - members: the wrapped handlers by name
 *   - [bubble]: bool, defaults to true
 *
 * - whatfailuregroup:
 *   - members: the wrapped handlers by name
 *   - [bubble]: bool, defaults to true
 *
 * - syslog:
 *   - ident: string
 *   - [facility]: defaults to 'user', use any of the LOG_* facility constant but without LOG_ prefix, e.g. user for LOG_USER
 *   - [logopts]: defaults to LOG_PID
 *   - [level]: level name or int value, defaults to DEBUG
 *   - [bubble]: bool, defaults to true
 *
 * - syslogudp:
 *   - host: syslogd host name
 *   - [port]: defaults to 514
 *   - [facility]: defaults to 'user', use any of the LOG_* facility constant but without LOG_ prefix, e.g. user for LOG_USER
 *   - [logopts]: defaults to LOG_PID
 *   - [level]: level name or int value, defaults to DEBUG
 *   - [bubble]: bool, defaults to true
 *   - [ident]: string, defaults to
 *
 * - swift_mailer:
 *   - from_email: optional if email_prototype is given
 *   - to_email: optional if email_prototype is given
 *   - subject: optional if email_prototype is given
 *   - [email_prototype]: service id of a message, defaults to a default message with the three fields above
 *   - [content_type]: optional if email_prototype is given, defaults to text/plain
 *   - [mailer]: mailer service, defaults to mailer
 *   - [level]: level name or int value, defaults to DEBUG
 *   - [bubble]: bool, defaults to true
 *   - [lazy]: use service lazy loading, bool, defaults to true
 *
 * - native_mailer:
 *   - from_email: string
 *   - to_email: string
 *   - subject: string
 *   - [level]: level name or int value, defaults to DEBUG
 *   - [bubble]: bool, defaults to true
 *   - [headers]: optional array containing additional headers: ['Foo: Bar', '...']
 *
 * - symfony_mailer:
 *   - from_email: optional if email_prototype is given
 *   - to_email: optional if email_prototype is given
 *   - subject: optional if email_prototype is given
 *   - [email_prototype]: service id of a message, defaults to a default message with the three fields above
 *   - [mailer]: mailer service id, defaults to mailer.mailer
 *   - [level]: level name or int value, defaults to DEBUG
 *   - [bubble]: bool, defaults to true
 *
 * - socket:
 *   - connection_string: string
 *   - [timeout]: float
 *   - [connection_timeout]: float
 *   - [persistent]: bool
 *   - [level]: level name or int value, defaults to DEBUG
 *   - [bubble]: bool, defaults to true
 *
 * - pushover:
 *   - token: pushover api token
 *   - user: user id or array of ids
 *   - [title]: optional title for messages, defaults to the server hostname
 *   - [level]: level name or int value, defaults to DEBUG
 *   - [bubble]: bool, defaults to true
 *   - [timeout]: float
 *   - [connection_timeout]: float
 *
 * - raven / sentry:
 *   - dsn: connection string
 *   - client_id: Raven client custom service id (optional)
 *   - [release]: release number of the application that will be attached to logs, defaults to null
 *   - [level]: level name or int value, defaults to DEBUG
 *   - [bubble]: bool, defaults to true
 *   - [auto_log_stacks]: bool, defaults to false
 *   - [environment]: string, default to null (no env specified)
 *
 * - sentry:
 *   - hub_id: Sentry hub custom service id (optional)
 *
 * - newrelic:
 *   - [level]: level name or int value, defaults to DEBUG
 *   - [bubble]: bool, defaults to true
 *   - [app_name]: new relic app name, default null
 *
 * - hipchat:
 *   - token: hipchat api token
 *   - room: room id or name
 *   - [notify]: defaults to false
 *   - [nickname]: defaults to Monolog
 *   - [level]: level name or int value, defaults to DEBUG
 *   - [bubble]: bool, defaults to true
 *   - [use_ssl]: bool, defaults to true
 *   - [message_format]: text or html, defaults to text
 *   - [host]: defaults to "api.hipchat.com"
 *   - [api_version]: defaults to "v1"
 *   - [timeout]: float
 *   - [connection_timeout]: float
 *
 * - slack:
 *   - token: slack api token
 *   - channel: channel name (with starting #)
 *   - [bot_name]: defaults to Monolog
 *   - [icon_emoji]: defaults to null
 *   - [use_attachment]: bool, defaults to true
 *   - [use_short_attachment]: bool, defaults to false
 *   - [include_extra]: bool, defaults to false
 *   - [level]: level name or int value, defaults to DEBUG
 *   - [bubble]: bool, defaults to true
 *   - [timeout]: float
 *   - [connection_timeout]: float
 *
 * - slackwebhook:
 *   - webhook_url: slack webhook URL
 *   - channel: channel name (with starting #)
 *   - [bot_name]: defaults to Monolog
 *   - [icon_emoji]: defaults to null
 *   - [use_attachment]: bool, defaults to true
 *   - [use_short_attachment]: bool, defaults to false
 *   - [include_extra]: bool, defaults to false
 *   - [level]: level name or int value, defaults to DEBUG
 *   - [bubble]: bool, defaults to true
 *
 * - slackbot:
 *   - team: slack team slug
 *   - token: slackbot token
 *   - channel: channel name (with starting #)
 *   - [level]: level name or int value, defaults to DEBUG
 *   - [bubble]: bool, defaults to true
 *
 * - cube:
 *   - url: http/udp url to the cube server
 *   - [level]: level name or int value, defaults to DEBUG
 *   - [bubble]: bool, defaults to true
 *
 * - amqp:
 *   - exchange: service id of an AMQPExchange
 *   - [exchange_name]: string, defaults to log
 *   - [level]: level name or int value, defaults to DEBUG
 *   - [bubble]: bool, defaults to true
 *
 * - error_log:
 *   - [message_type]: int 0 or 4, defaults to 0
 *   - [level]: level name or int value, defaults to DEBUG
 *   - [bubble]: bool, defaults to true
 *
 * - null:
 *   - [level]: level name or int value, defaults to DEBUG
 *   - [bubble]: bool, defaults to true
 *
 * - test:
 *   - [level]: level name or int value, defaults to DEBUG
 *   - [bubble]: bool, defaults to true
 *
 * - debug:
 *   - [level]: level name or int value, defaults to DEBUG
 *   - [bubble]: bool, defaults to true
 *
 * - loggly:
 *   - token: loggly api token
 *   - [level]: level name or int value, defaults to DEBUG
 *   - [bubble]: bool, defaults to true
 *   - [tags]: tag names
 *
 * - logentries:
 *   - token: logentries api token
 *   - [use_ssl]: whether or not SSL encryption should be used, defaults to true
 *   - [level]: level name or int value, defaults to DEBUG
 *   - [bubble]: bool, defaults to true
 *   - [timeout]: float
 *   - [connection_timeout]: float
 *
 * - insightops:
 *   - token: Log token supplied by InsightOps
 *   - region: Region where InsightOps account is hosted. Could be 'us' or 'eu'. Defaults to 'us'
 *   - [use_ssl]: whether or not SSL encryption should be used, defaults to true
 *   - [level]: level name or int value, defaults to DEBUG
 *   - [bubble]: bool, defaults to true
 *
 * - flowdock:
 *   - token: flowdock api token
 *   - source: human readable identifier of the application
 *   - from_email: email address of the message sender
 *   - [level]: level name or int value, defaults to DEBUG
 *   - [bubble]: bool, defaults to true
 *
 * - rollbar:
 *   - id: RollbarNotifier service (mandatory if token is not provided)
 *   - token: rollbar api token (skip if you provide a RollbarNotifier service id)
 *   - [config]: config values from https://github.com/rollbar/rollbar-php#configuration-reference
 *   - [level]: level name or int value, defaults to DEBUG
 *   - [bubble]: bool, defaults to true
 *
 * - server_log:
 *   - host: server log host. ex: 127.0.0.1:9911
 *   - [level]: level name or int value, defaults to DEBUG
 *   - [bubble]: bool, defaults to true
 *
 * All handlers can also be marked with `nested: true` to make sure they are never added explicitly to the stack
 *
 * @author Jordi Boggiano <j.boggiano@seld.be>
 * @author Christophe Coevoet <stof@notk.org>

That is a lot. Some are obvious, others you will need to google and figure out.

You can get as wild and creative as you want though.

Config location is everything

Where you place your configuration is very important. A little more hint here.

If you want to log just during Development you put the configuration inside the dev folder. /app/config/packages/dev/monolog.yaml

If you want to log only during production you put the configuration in the prod folder. /app/config/packages/prod/monolog.yaml

If you want to log in all environments you place the configuration in the main configuration folder. /app/config/packages/monolog.yaml

So if you want a specific logger that logs to a specific file to be available in all environments define it in the “packages” folder.

It isn’t too bad once you figure it out.

Here is the official documentation on how configuration works. Read it if you need more information.

An example configuration

Before getting too much further lets look at the default file I have for the configuration of monolog in the development environment.

#file name -> /config/packages/dev/monolog.yaml
monolog:
    handlers:
        main:
            type: rotating_file
            max_files: 3
            path: "%kernel.logs_dir%/%kernel.environment%.log"
            #path: php://stderr
            level: debug
            channels: ["!event"]
        # uncomment to get logging in your browser
        # you may have to allow bigger header sizes in your Web server configuration
        #firephp:
        #    type: firephp
        #    level: info
        #chromephp:
        #    type: chromephp
        #    level: info
        console:
            type: console
            process_psr_3_messages: false
            channels: ["!event", "!doctrine", "!console"]

Yours may differ, I have probably made changes to this and forgotten at this point or some changes in the Symfony code base may have happened.

After the word handlers you list each handler one tab level in.  Each handler gets it’s own configuration settings. Basically you pick a handler from the long list above, then configure it properly for your needs.

For local development I prefer the rotating_file option. Since it seems like it is the only one that you can put a limit on the number of files.

The stream option just infinitely fills a single file forever and ever. Unless you have some sort of log rotator as mentioned in the documentation.

If you have the time and urge you could even store the messages in a database then create a user interface in your admin panel to view the messages then delete them. Otherwise you will need some way of limiting the logs and log sizes.

The documentation refers to this list as a stack of handlers. You can have as many as you want, like I mentioned above.

Here is the default production configuration taken from here in the Symfony docs.

# file name -> /config/packages/prod/monolog.yaml
monolog:
    handlers:
        filter_for_errors:
            type: fingers_crossed
            # if *one* log is error or higher, pass *all* to file_log
            action_level: error
            handler: file_log

        # now passed *all* logs, but only if one log is error or higher
        file_log:
            type: stream
            path: "%kernel.logs_dir%/%kernel.environment%.log"

        # still passed *all* logs, and still only logs error or higher
        syslog_handler:
            type: syslog
            level: error

The Symfony docs say the following about production logs.

In the prod environment, logs are written to STDERR PHP stream, which works best in modern containerized applications deployed to servers without disk write permissions.

I’ll probably remove the line that logs locally to a file the file_log line. No need to infinitely fill a log I don’t read. You will need to decide what handlers you want in production and configure them.

Handler names

As you can see in the development configuration file above, there is a handler named “main”. You can give your handlers any name you want, but you need to tell Symfony what type it is with the type directive like you see in main.

main: 
  type: rotating_file 
  max_files: 3 
  path: "%kernel.logs_dir%/%kernel.environment%.log" 
  level: debug 
  channels: ["!event"]

The word “main” can be anything you desire, but notice how you must use type, then use one of the above handler names, then provide the options. Read more about handler names here in the Symfony docs. Basically you can call the handler anything even “asdfg” as long as you use the word “type” and list the type and other required information. More on this below.

Bubble?

That is the question!

One property that you will see on all of the types above is “bubble” Further digging into the github monolog documentation reveals the following about it.

Handlers also have a $bubble property which defines whether they block the record or not if they handled it. In this example, setting the MailHandler‘s $bubble argument to false means that records handled by the MailHandler will not propagate to the StreamHandler anymore.

Basically this means you need to define your loggers in the proper order if you want to have the handling stop at a particular logger for a particular situation. Otherwise each logger in your list does something each time and you might not want that. Like do you want errors to go to a log and also to the console during development?

One thing you may want to think about is where do you want your errors, exceptions and other logged data to be logged? This answer will be different for production vs development for sure. You may want to log just exceptions to a database in production, so that you can query it and show them in an admin user interface for example.

This part of the documentation speaks about logging to different locations.

Channels aka logging to multiple places

What are these channels?

The documentation speaks of channels. What are these channels? In the code above for the monolog.yaml development configuration you will see this line

 channels: ["!event", "!doctrine", "!console"]

Here in the documentation it says the following about channels

The Symfony Framework organizes log messages into channels. By default, there are several channels, including doctrine, event, security, request and more. The channel is printed in the log message and can also be used to direct different channels to different places/files.

Aha. So that is what those are. I wonder what the ! means? Does that mean it ignores those or does it mean it accepts those? Yes it does. Basically the above line would log everything except (event, doctrine and console.) Below you can see a list of all the channels.

So to clarify if you want a handler to log a message for a specific channel you can use the following syntax in the handler configuration.

channels: [security, doctrine]

If you want the handler to ignore a channel then you use this syntax in the handler configuration

channels: ["!event", "!doctrine", "!console"]

You can really get as insanely complex as you want with this logging stuff.

By the way how do you know what logging channels are available?

I prefer the following command, because these can be auto-wired :

php bin/console debug:autowiring

Scroll down and look for monolog listings. You will see something like this.(These can all be auto-wired)

console bin output

These are the channels you have.Basically whenever one of these experiences an issue Symfony sends out an event and the handlers you list decide if they want to do something about it. This way using the channels and multiple handlers you can log things exactly where you want them.  You can even log the same event/message multiple times in multiple locations.

As the docs mention, by default Symfony logs all events to the same location. It is up to you to change that to your projects needs.

Another useful command is the following which allows you to get detailed info about each, it also lists more, but not all are auto-wireable.

php bin/console debug:container monolog

That is interactive and you can choose one of the listings to get more information about it. I find the first method useful to find what I actually can access.

Creating Custom Channels

Sometimes you need to log a specific issue to a specific file or location. For this you need to create your own Channel. The documentation is really weak on this subject so lets dig deep into it.

I get tired of the foo bar BS so here I create a custom channel named “chicken” just so it sticks out for this example.

To create the channel you simple list it at the top of the monolog.yaml file like this.

monolog:
  channels: ['chicken']
  handlers:
    main:
      type: rotating_file
      max_files: 3
      path: "%kernel.logs_dir%/%kernel.environment%.log"
      #path: php://stderr
      level: debug
      channels: [ "!event" ]

A little confusing how you use that syntax in more than one location. In one it listens for channels in another it creates channels.

Wait what?

Maybe channel_list or something else to differentiate would have been better?

Placing the directive above the rest of the handler code creates the new channel or channels.  I don’t know if this is a requirement, but I know it works. To create more than one channel create strings separated by commas.

Now to check if anything happened I will use this command


php bin/console debug:autowiring

Now we have a chicken logger.

Now you can see “monolog.logger.chicken” logger that was created with that one line in the monolog.yaml configuration file.

That is how it is done.

How to use the logger?

There are two ways to use the logger, one you didn’t have to do any of this to use, the other is autowiring, which is why we did all of this for. The other way is to just initiate it and pass the values to the constructor like the documentation for Monolog shows.

Even though all that configuration seems like a pain, in the end it makes things easier by allowing us to auto-wire the logger we want. This saves a lot of having to create the same boilerplate code over and over all throughout our app.

Taking another look at the output of the bin/console debug:container command from above we can see the following

Lets now use the chickenLogger

Each line after the first monolog.logger contains what looks like a PHP variable. It is in fact a PHP variable, one you can use for type hinting. How this works is you type hint LoggerInterface, then use which variable you want and Symfony automagically loads it for you.

farth too easy meme
It feels too easy to be true

Back to the chickenLoggerExample. What I want to do with the chickenLogger is use it to log exceptions to their own exceptions log for easier viewing. This is useful in a situation like using a library to create and convert images.

class UserImageProcessor
{

    private LoggerInterface $chickenLogger;

    public function __construct(LoggerInterface $chickenLogger){
        $this->chickenLogger = $chickenLogger;
        $this->chickenLogger->debug('lets log some chickens');
    }

See in the constructor how I passed the variable $chickenLogger and now Symfony passes me the chickenLogger Channel and I can now log chickens. Logging chickens is a very handy feature in the real world.

And below is the whole monolog.yaml file I am using to log my chickens.

monolog:
  channels: ['chicken']
  handlers:
    main:
      type: rotating_file
      max_files: 3
      path: "%kernel.logs_dir%/%kernel.environment%.log"
      #path: php://stderr
      level: debug
      channels: [ "!event", "!chicken" ]

    exceptions:
      type: rotating_file
      max_files: 2
      path: "%kernel.logs_dir%/exceptions.log"
      file_permission: '765'
      level: debug
      channels: ['chicken']


    # uncomment to get logging in your browser
    # you may have to allow bigger header sizes in your Web server configuration
    #firephp:
    #    type: firephp
    #    level: info
    #chromephp:
    #    type: chromephp
    #    level: info
    console:
      type: console
      process_psr_3_messages: false
      channels: [ "!event", "!doctrine", "!console" ]

Notice I used the file permissions option. That is a handy one. See the exceptions handler is using the chicken channel. Again I could have named the “exceptions:” section anything even “chirpy-birds” symfony doesn’t care, only the directives matter.

Now when I log my chickens, the main handler section will ignore the chickens, but my exceptions section will log my chickens and so will the console. I’d have to add “!chicken” to the consoles “channels” section or I can add “bubbles: false” to the exceptions section to stop it.

Now you know!!!

Links about Loggers

Logging to Rollbar -> Helps you organize and view logs and much more.

Logging with Monolog -> A short article

Logging -> Documentation link ->overall general information

How to log messages to different files -> more from the documentations

Using the logger -> more from the docs on how to use it.

Categories
Software Development Web Development

PHP Enumeration classes vs fake enumerations what to use.

I am growing to HATE PHP ENUMS. Why? Because if you forget to call ->value you get BS errors about cannot convert object to string. It is just another un-needed step. Enums sounded cool at first but fuq the BS with calling ->value, it is entirely too easy to forget.

Enumeration classes in PHP are a new feature as of version 8.1 Fake enumerations were what we did before this new feature. A fake enumeration was just a class with constants, the syntax to refer to them was the same minus ->value. See my other article if you don’t know what fake PHP enumerations are Php Backed Enums don’t forget to call value

TLDR

Use plain classes with constants if any of the values will repeat. Use Enumerations when each value is always unique. Keep reading to find out WTF I am talking about.

WTF are you talking about?

The story

The new Enumeration classes have a limitation on them that each “case” must have an unique value, it is just how it works.
This is only a problem when you are using Enumerations more like the old fake constant enumerations, because you can’t give two cases the same value. If you are using ints you run into this issue more quickly.

Let me show an example.

I have often used this pattern to help eliminate bugs from typing, it also helps you remember what values are possible for something. Below is the values allowed for an image for some sort of upload.

class ImageDataEnum
{
    const HEIGHT = 'height';
    const SIZE_STRING = 'size';
    const IMAGE_URL = 'image_url';
    const WIDTH = 'width';
}

This way I know what values are allowed in the upload and I don’t get errors from typing.This would convert to an Enumeration perfectly.

I have dysgraphia and dyslexia so this is something I fight constantly, it is also why I hate Linux command line. Basically my brain gets the letters out of order sometimes when I type or write or leaves entire letters out or adds them and my eyes don’t see it for some odd reason sometimes. I couldn’t take notes in school because it turned into jibberish word soup.

But I also like to use them to store info that won’t change, like maximum image size. This is a use case that won’t work with PHP Enumerations, probably one of the only ones I can think of.

Bad code

You made a real mess of that code fella

This kind of code won’t work.

enum ImageSizeEnum: int
{

    case BLOG_IMAGE_HEIGHT = 600;
    case BLOG_IMAGE_WIDTH = 600;
    case TWITTER_LARGE_IMAGE_HEIGHT = 600;
    case TWITTER_LARGE_IMAGE_WIDTH = 1200;
    case PROFILE_PICTURE_HEIGHT = 180;
    case PROFILE_PICTURE_WIDTH = 180;
    case PROFILE_THUMB_HEIGHT = 55;
    case PROFILE_THUMB_WIDTH = 55;
    case USER_THUMB_HEIGHT = 150;
    case USER_THUMB_WIDTH = 150;
    case USER_IMAGE_HEIGHT = 600;
    case USER_IMAGE_WIDTH = 600;

}

Notice that while each case has a unique name, they do not have unique values. The code above will give you an error because the values for the cases is repeated. If you are using PHPStorm then it is screaming at you right now with red underlines and error warnings etc. LOLOL

PHP Storm is all like…

The docs say the following

In PHP, Enums are a special kind of object. The Enum itself is a class, and its possible cases are all single-instance objects of that class. That means Enum cases are valid objects and may be used anywhere an object may be used, including type checks

In order to do what is intended in the code above (a list of constants to be used in code later) this has to be converted back to a class with constants. It looks so similar it is crazy.

Just convert really quickly

Ok code

This code will work.

class ImageSizeEnum
{

    const BLOG_IMAGE_HEIGHT = 600;
    const BLOG_IMAGE_WIDTH = 600;
    const TWITTER_LARGE_IMAGE_HEIGHT = 600;
    const TWITTER_LARGE_IMAGE_WIDTH = 1200;
    const PROFILE_PICTURE_HEIGHT = 180;
    const PROFILE_PICTURE_WIDTH = 180;
    const PROFILE_THUMB_HEIGHT = 55;
    const PROFILE_THUMB_WIDTH = 55;
    const USER_THUMB_HEIGHT = 150;
    const USER_THUMB_WIDTH = 150;
    const USER_IMAGE_HEIGHT = 600;
    const USER_IMAGE_WIDTH = 600;

}

With the fake enum you leave off the ->value part when you need the value as noted in this article. Php Backed Enums don’t forget to call value

Now I can have constants that have the same value and PHP won’t barf errors on me.

kitten frew up meme
I make PHP barf.

You can also do something very similar in Javascript if you are interested check out this article. Faking Enumerations with Vanilla javascript

Summary

So basically if you need to use the same value with a different name, then you need the old fashioned PHP fake enumerations. If your use case has it so that every named case has a unique value then use Enumerations.

 

Categories
Software Development Web Development

Javascript and contenteditable how to move the cursor to the end of user input

This appears to work in all browsers. Let me know if you find issues.

So this is something I needed to do and all the examples I found didn’t work properly. Or they worked partially, or only in one browser.

The idea seems super simple, but in reality the problem becomes complex for several reasons. For one, you need to have event listeners to listen for input then perform the proper actions to update the content and move the cursor to the end.

My use case makes it even harder and maybe yours does too. What I needed was to allow a user to enter hashtags and then send an ajax request to the backend to fetch a list of matching hashtags to display in a drop list below the input(auto suggestions). Then when the user selects one, add it to the list by removing the typing first, then add the new hashtag and move the cursor to the end.

At first I was all confident like

This should be easy right?

So that sounded easy. I got most of it working by using Google, reading some articles, reading stack questions… but it wasn’t working properly. My cursor would appear in random places or not appear, or not be where I wanted it to be. I tried reading the MDN docs for range and well I didn’t quite grasp how the system works.

If you are reading this you probably have done all this too and may be having issues as well.

Trying to get this working

One stupid, nonsensical issue I ran into right away was when the user hit backspace instead of one letter or hashtag being removed, ALL CONTENT WAS REMOVED.

homer simpson meme
Not the action we expected now is it.

I fixed this by adding &nbsp; which is html for non breaking space. Explained in depth in this great article. Some people add an empty paragraph from what I read. I decided to use nbsp; because it is seen as a #text node not a text element like p for example.

So now when the user backspaces, the first backspace does nothing, but the next removes the hashtag. This repeats until there are no hashtags. I was using divs, with spans inside them. I moved to just a simple span element with @nbsp; added to the end.

My original html minus class and id’s

My first idea was to have a hashtag with an x to remove it. But then I was like, most people will just backspace and if they are on their phone no way they can accurately touch it.

I also discovered that you must add contenteditable=false to each element within the parent element so that when the user clicks, the cursor doesn’t appear inside some randomish point instead of at the end of the content.

So I adjusted it to be just the span with a following nbsp tag like this.

Ok so now I have these hashtags displaying correctly. I don’t like that I have to add the invisible space tag to make the contenteditable act right, but what can we do?

If you are wondering how I make these cool code images, you need to checkout this carbon app.

Below is the actual div and some sample hashtags I pulled from the developers console.

notice the html structure here.

See that autocomplete=off . Yeah you need that so that when a user types into input or contenteditable divs it doesn’t drop it’s own suggestion list over your suggestion list, it really makes things a mess.

Moving the cursor to the end.

In the video below you can see what I want and what I have gotten working so far. This pretty much works exactly how I wanted it to work. Notice the user can enter a hashtag at the beginning and when they choose enter the cursor moves to the end.

If you are working on a similar project and need the cursor to go to the end like this, then you know what a pain it was to get this figured out. LOL

I eventually got tired of the try, fail, google, curse, try, fail, curse some more routine. So I decided to dig directly into the W3C standards myself. It started making sense after  I read it a few times and then checked the MDN links above again and played around etc. I tried reading the DOM spec but I didn’t find it as helpful. But overall it took reading all of everything I could find and trial and error to figure it out.

Ok so what magical code have I used? I wont cover the entire class and how it works. I’ll just show the actual method that creates a range and moves to the end.

moveCursorAfterHashtags() {
        let hashtagRange = document.createRange();
        let windowSelection = window.getSelection();
        //remove any previously created ranges
        windowSelection.removeAllRanges();
        let theNodes = this.hashtagsDivElement.childNodes;
        this.hashtagsDivElement.focus();
        let firstNode = theNodes[0];
        let lastNode = theNodes[theNodes.length - 1];
        let start = theNodes[0];
        let end = theNodes[theNodes.length - 1];
        console.log('Start is ' + start.nodeName + ' end is ' + end.nodeName + " node count " + theNodes.length);
        hashtagRange.setStartBefore(firstNode);
        hashtagRange.setEndAfter(lastNode);
        hashtagRange.collapse(false);
       //add the range to a window selection object.
        windowSelection.addRange(hashtagRange);
        windowSelection.collapseToEnd();
        console.log('commonAncestorContainer contains ' + hashtagRange.commonAncestorContainer.id);
        console.log('The start container is ' + hashtagRange.startContainer.id);
        console.log('The end container is ' + hashtagRange.endContainer.id);
    }

Notice all the console logs needed to figure this out. LOL

So the code above creates a range. Then creates a selection object, this is needed to actually move the cursor to the end of the range with this line

windowSelection.collapseToEnd();

But before you move the cursor to the end you must collapse the range to a single point hence the line

 hashtagRange.collapse(false);

You will see people using setStart() and setEnd(). After reading the docs I found it MUCH EASIER. to just use setStartBefore() and setEndAfter().

This is because of the structure I am using a span followed by a plain #text node, which may or may not exist, and it makes calculating the offsets harder. This way if the user backspaces one time and the nbsp is removed, but the leading hashtag remains the range is still inclusive and less calculations needed.

so much thinking…

You can see I feed them a node. The first and last node are calculated with

let firstNode = theNodes[0];
let lastNode = theNodes[theNodes.length - 1];

hashtagRange.setStartBefore(firstNode); 
hashtagRange.setEndAfter(lastNode);

This is because .childNodes returns an array which starts with 0, so to get the last one you must reduce the count by 1. A 2 item array will have the values 0 and 1, because the numbering starts at 0.

In this terminology node is anything html element or just typed text or the nbsp; that makes the space. These are all called nodes.

The other start and end are old code just used for console logging to see WTF is going on LOL

Also notice where I set the focus to the parent div element before creating the range etc.

this.hashtagsDivElement.focus();

I am not sure if this makes a difference, I prefer to add focus to the element first.
And that is how you get the cursor to set at the end of the contenteditable div.

 

boom how it is done baby meme
Trial and error until it works

 

If this doesn’t work properly in all browsers it is the browser makes fault for not deciphering the cryptic standards better.

Categories
Software Development Web Development

Symfony how to get query string values sent by AJAX calls

When using a Symfony API endpoint for getting values such as maybe something like an auto-complete feature you will need to be able to send some text(what the user enters) to the backend. Then have the backend return a response based on that.

For example with an auto complete feature, you might want to send 3 characters to an API endpoint and have it only return a list of words that start with those three characters, instead of returning every last word in the database. This will use less of your servers resources and the users while speeding your app up.

The way I suggest to do this is to first use FosJsRouting bundle in your Javascript to create the URL’s for you. This makes it much easier, faster ( Once you get it installed etc ) and consistent. Here is a long article I wrote about installing and using FosJsRouting bundle How to get URL Routes in your Javascript in Symfony 5+.

This is a short example and doesn’t include checking if the user is logged in ( has rights) if it is an AJAX request, check headers etc. it is just for this example.



/**
     * returns a list of the users image collection names
     * @Route("/get_image_collections", name="get_image_collections", options={"expose"=true},  methods={"GET"})
     */
    public function get_image_collections(Request $request): JsonResponse
    {
        $query = $request->getQueryString();
      $text2 = $request->query->get('text');
      $text =  $request->get("text");
        $values = array(
            'first',
            'second',
            'third',
            'fan',
            'free',
            'narcotics',
            'arse',
            'tardigrade',
            'tinnitus',
            'monkey',
            'mall',
            'doppleganger',
            'ballocks',
            'zoo',
            $query,
            $text,
            $text2
        );
        return new JsonResponse($values);
    }

This is very simple, it doesn’t even hit a database, it is just for this article and testing. All this does right now is return the list and add some text and query so I can see what was sent. In production you might even want to use caching so that your database only gets hit if it has to.

As you can see there appears to be two ways to get the value of the query. Both worked for me, but I am thinking that going through the query method is the proper way like follows.

$request->query->get('text');

The query being used for this endpoint looks like this

"http://sogi-test/get_image_collections?text=fa"

In the future I’ll update the code to include security checks, to return a matching list compiled from a database query and caching.

But notice the (Request $request) that is Dependency Injected ( see symfony dependency injection )

I should also add, below is how I am building the URL with FosJsBundle

let url = Routing.generate('get_image_collections', {text: text});

That is where the “text” named query key is coming from. You can name the key anything you want. If you have more than one named value then use a comma and add it after the first, like in the FosJsRoutingBundle docs under how to use above link.
For example say you want to include something like a page number for paging through results or limiting them etc. You could create the
url route like this

let url = Routing.generate('get_image_collections', {text: text, limit: limit});

This Javascript JSON looks confusing, the first part is the name(key) the second is a(value) variable containing the value
Then to get the values in your controller route you do this.


$text = $request->query->get('text');
$limit = $request->query->get('limit');

You can send as many values as you need to this way. Forms work a little differently. I’ll write an article about those soon too.

And that is how you get the values you send.
baby how it is done meme
That is how it is done

Interesting Error story

I figured I would mention this debugging story I had while sending an ajax request with an improperly formed URL ( aka not matching the route)

I kept getting errors when trying to use Promise.json() with the value returned from my AJAX call. What happened is the server was returning HTML instead of JSON which results in an error when Promise.json() is called.

A while of that and thinking it was my JS code but not really sure of WTF was going on. I finally started outputting my URL to see what was being created and low and behold that was it. I was even watching the Request/Response in the browser. But, it was not obvious until I really started inspecting the created URL vs the route output that I figured out my route and url didn’t match and that was the reason for the redirect.

How to view Symfony routes

 

My route didn’t match because at some point I changed how I wanted the route to look and I had not rerun the FosJsRouting bundle as mentioned in that article to renew the routes in it’s json file.  So it created a URL that didn’t match, the route was created like /route/{text} but the api route expected /route&text=….

I didn’t get an error. Nope and nothing was recorded that I could find in the error logs. Symfony simply redirected ( 301) sent back to my ajax which then somehow did another request to the page I was using the javascript in,  returning the pages HTML.

I was like

Uhm, wait… what?

I have no idea why Symfony redirects when the route doesn’t match.  I don’t even know what setting to adjust where to change this, I know I did something somewhere at sometime… I’ll have to dig into what I did.

Why this happend is because I didn’t re-update the FosJsRouting json file and then restart webpack so it pulls in the new file. But I know there is some sort of setting somewhere for Symfony for the redirect action I am seeing and I know I set it somewhere somehow.

Can I have hamburger now???

Categories
Software Development Web Development

Php Backed Enums don’t forget to call value

The one thing I don’t like about new PHP enums is, if you forget to call ->value you get exceptions “object can’t be converted to string” It is entirely too easy to forget to call ->value.

This means in places where I refactor code I have to remember to call ->value. Hence the article title “Php Backed Enums don’t forget to call value”

It is really easy to forget to call ->value when using these new Enums.

Well thanks to my IDE PhpStorm, I caught this error before it happened to me… in most places… most times. Nah not really I forget to call ->value all the time.

I like the concept of having an Enum class as up until version PHP 8.1 you had to create class constants and pretend they were real Enums.

Old php enums

Old PHP Enums Example

Here is how we used to do PHP Enums for forever until version 8.1

class ImageDataEnum
{
    const HEIGHT = 'height';
    const SIZE_STRING = 'size';
    const IMAGE_URL = 'image_url';
    const WIDTH = 'width';
}

And to use that in any code you simply did the following where you needed a value.

$height = ImageDataEnum::HEIGHT;

And inside $height would be the string “height” you could use this to make sure a value exists without having to spell it out every time, reducing the likelihood of bugs. This is very straight forward and easy. You can still add constants to Enum classes and use them, but it feels better using case instead.

New Enums

A backed enum looks like this. Note const is now case, class is now enum, but the rest is about the same.

enum ImageDataEnum: string
{
    case HEIGHT = 'height';
    case SIZE_STRING = 'size';
    case IMAGE_URL = 'image_url';
    case WIDTH = 'width';
}

Notice the word “string” you can use int or string but not a combination of both. Backed Enums Docs here.

Now to use the new Enums like the code above you do like this

$height = ImageDataEnum::HEIGHT->value;

Otherwise $height will be an object, one that contains  handy built in methods try() and tryFrom(). See the doc links for more info on that. You can also define your own methods.

But if you fail to call ->value and you try to use this for a string comparison you will get oopsies. You can use the IDE to hunt down all cases of the old class type enums.

//this won't work
if('height' === ImageDataEnum::HEIGHT ){
 //code to do stuff in here
}

The above will result in an error telling you the comparison is not possible. You can’t compare a string to an object.

//this will work
if('height' === ImageDataEnum::HEIGHT->value ){
 //code to do stuff in here
}

You can also call ImageDataEnum::HEIGHT->name which will return HEIGHT. So you can get the name and value using those methods.

Another nice thing about the new Enum classes is they are full on classes, you can add methods to them if you want. Like checking if a value matches any of the case values or whatever your use case is.

Enums are really handy for limiting what values can be entered by users and checking against them. Another good use I have found is creating a list of options for a Database table column.

Here is an example of a column in one of my tables that stores a medias content rating type. The system later uses this in many places to make sure that the media is of this type or that the user wants to see this type of media.

enum ContentRatingsEnum: string
{
    case EVERYONE = 'everyone';
    case MATURE = 'mature';
    case RATED_X = 'rated-x';
}

This column in a media table can only contain these values and users can only select from these values as their content preference type. This is helpful because I don’t have to type those strings in 100,000 places and when I need to change one I simply refactor with my IDE features.

Here is an excellent video that just came out about PHP ENUM’s the start of the video is anyways.

Categories
Software Development Web Development

Symfony 5+ Twig templates don’t forget to call the parent

Twig templates use inheritance and allows you to create named sections like this.


<!doctype html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">

    <link rel='icon' href="{{ asset('images/favicon.ico') }}" type='image/x-icon' >
    {% block head_extra %}{% endblock %}
    <title>{% block title %}{{ title }}{% endblock %}</title>
    {% block stylesheets %}
        {# 'app' must match the first argument to addEntry() in webpack.config.js #}
        {{ encore_entry_link_tags('app') }}
    {% endblock %}
</head>
<body>

<main>
    <div class="container">
       {% block navbar %}{{ include('_nav_bar.html.twig') }}{% endblock %}
        {% block body %}{% endblock %}
        {% block javascripts %}
            {# these files are needed for getting url routes in javascript  #}
            <script src="{{ asset('bundles/fosjsrouting/js/router.js') }}"></script>
            <script src="{{ path('fos_js_routing_js', { callback: 'fos.Router.setData' }) }}"></script>
            {# 'app' must match the first argument to addEntry() in webpack.config.js #}
            {{ encore_entry_script_tags('app') }}
        {% endblock %}

        {% block javascript_extra %}{% endblock %}
    </div>
</main>
</body>
</html>

And then you can reuse/inherit this template in another template like this.

{% extends 'base.html.twig' %}
{% block title %}Register{% endblock %}

{% block stylesheets %}
{{ parent() }}

{% endblock %}
{% block body %}

{{ form_start(registrationForm) }}
{{ form_errors(registrationForm) }}
{{ form_row(registrationForm.email) }}
{{ form_row(registrationForm.emailMatch) }}
{{ form_row(registrationForm.plainPassword) }}
{{ form_row(registrationForm.passwordMatch) }}
{{ form_row(registrationForm.userAlias) }}
{{ form_row(registrationForm.agreeTerms) }}

{{ form_end(registrationForm) }}

{% endblock %}
{% block javascript_extra %}

{{ encore_entry_script_tags('registration') }}

{% endblock %}

Notice the extends keyword and notice this section

{% block stylesheets %}
{{ parent() }}

{% endblock %}

Notice the use of the “parent” keyword here.

{% block stylesheets %} {{ parent() }} {% endblock %}

Any other css code you want included must come after the parent call like this.


{% block stylesheets %}
    {# 'app' must match the first argument to addEntry() in webpack.config.js #}
    {{ parent() }}
    {{ encore_entry_link_tags('editAboutUser') }}
    {{ encore_entry_link_tags('sogiDraw') }}
{% endblock %}

If I didn’t call “parent()” above then only the CSs for editAboutUser and sogiDraw would be included app would not be included. The CSS for app which is called in the base template in the stylesheets section would not be included without the call to parent().

So if some of your CSS is not working, then the reason is you probably forgot to call parent. This call to parent is saying “Include what was in the parent stylesheet section”. Otherwise without the call you will be missing Styles and be saying WTF?

Don’t forget to call parent