Categories
Software Development Web Development

Symfony 5+ how to create a Maker

The docs do not mention how to create your own maker bundle. It gives a link to some of the Github code of the existing maker bundles for you to view as an example.

you need to figure it out meme
Here is a link, you figure it out. LOL

So when I dug deeper into the subject, I figured out what you are actually doing with a maker is creating a console app. You will know these “Makers” from typing commands on the command line like the following:

php bin/console cache:clear  or 
php bin/console list make

So step 1 is to create a new class in a directory in your “src” directory. I call mine… “Makers”

Then inside your class you need to add some use statements for the classes you intend to use like so.


use Symfony\Bundle\MakerBundle\ConsoleStyle;
use Symfony\Bundle\MakerBundle\DependencyBuilder;
use Symfony\Bundle\MakerBundle\Generator;
use Symfony\Bundle\MakerBundle\InputConfiguration;
use Symfony\Bundle\MakerBundle\Maker\AbstractMaker;
use Symfony\Bundle\MakerBundle\Validator;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;

Here is a link to the Console classes so you can find out what is available.
For the other classes you see e.g. “Symfony\Bundle\MakerBundle\ConsoleStyle  It appears these are located in “maker-bundle/src/class-name/ now. Here is a link to the related classes source code.

Once you have decided what classes you will need, via trial and error based on your project idea, you next need to extend the AbstractMaker interface and add the methods like this.

Below is an example of a maker that extracts table information from a database and creates classes with it. I use these classes for creating custom queries. It is easier than remembering all the fields of a table and what values they can contain etc. The only downside is when changes to a table are made and the column name you used no longer exists, you have to find it in your code. But a good IDE will help you find this. I might add this code to a repo or maybe Symfony one day. This is useful for writing custom queries where you need to know the column names and types etc. You can use this with Doctrine CRUD classes or your own etc.

I made notes on what I think the methods do since there is absolutely 0 documentation that I can find.


<?php


namespace App\Maker;


use App\DbUtils\MysqlConnection;
use App\Schema\SchemaMaker;
use Symfony\Bundle\MakerBundle\ConsoleStyle;
use Symfony\Bundle\MakerBundle\DependencyBuilder;
use Symfony\Bundle\MakerBundle\Generator;
use Symfony\Bundle\MakerBundle\InputConfiguration;
use Symfony\Bundle\MakerBundle\Maker\AbstractMaker;
use Symfony\Bundle\MakerBundle\Validator;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;

class TableMetaMaker extends AbstractMaker
{

    /**
     * Return the command name for your maker (e.g. make:report).
     */
    public static function getCommandName(): string
    {
        return 'make:table-meta';
    }

    /**
     * Configure the command: set description, input arguments, options, etc.
     *
     * By default, all arguments will be asked interactively. If you want
     * to avoid that, use the §inputConfig->setArgumentAsNonInteractive() method.
     * @param Command §command
     * @param InputConfiguration §inputConfig
     */
    public function configureCommand(Command §command, InputConfiguration §inputConfig)
    {
        §command->setDescription('Creates classes with table data')
            ->addArgument('schema', InputArgument::REQUIRED, sprintf('The name of database schema to model '));
    }

    /**
     * Configure any library dependencies that your maker requires.
     * @param DependencyBuilder §dependencies
     */
    public function configureDependencies(DependencyBuilder §dependencies)
    {
        // TODO: Implement configureDependencies() method.
    }

    /**
     * Called after normal code generation: allows you to do anything.
     * @param InputInterface §input
     * @param ConsoleStyle §io
     * @param Generator §generator
     */
    public function generate(InputInterface §input, ConsoleStyle §io, Generator §generator)
    {
        §schema = §input->getArgument('schema');
        §io->success("Now generating Table classes for schema " . §schema);
        §userName = §_SERVER['DATABASE_USER'];
        §pass = §_SERVER['DATABASE_PASSWORD'];

        §mysql = new MysqlConnection(§userName,§pass, §schema);
        §schemaMaker = new SchemaMaker(§mysql,§schema);
        §schemaMaker->setSchemaTableList();
        §schemaMaker->createAllSchemaClasses();

        foreach (§schemaMaker->getTablesList() as §table){
            §io->success("Created Class for table " . implode('-', §table));
        }

    }

    public function interact(InputInterface §input, ConsoleStyle §io, Command §command)
    {
        §io->title('Create the database table classes...');
        §value = '';

        if (null === §input->getArgument('schema')) {
            §argument = §command->getDefinition()->getArgument('schema');
            §question = new Question(§argument->getDescription());
            §value = §io->askQuestion(§question);
            §input->setArgument('schema', §value);

        }
        §value = §input->getArgument('schema');
        §io->success("Table classes were made from schema " . §value);

        /*§input->setArgument('schema', §io->ask(
            'What is the name of the database schema',
            null,
            [Validator::class, 'notBlank']
        )
        );*/
    }
}

As you can see there are some custom classes I am using too. I don’t have the time right now to cleanup and publish all the code. Maybe one day though. I have several other Makers I use. One clones Entities, another creates Repositories based on Entities. Those two I may publish sooner as they are the most useful and would require the least effort to share.

Since all you are doing with a Maker is creating a console app. I’d recommend reading all the links below about that. When you extend

AbstractMaker

Here is an absolute basic class with the basic methods you must have for your maker to work.


<?php

namespace App\Maker;

use Symfony\Bundle\MakerBundle\ConsoleStyle;
use Symfony\Bundle\MakerBundle\DependencyBuilder;
use Symfony\Bundle\MakerBundle\Generator;
use Symfony\Bundle\MakerBundle\InputConfiguration;
use Symfony\Bundle\MakerBundle\Maker\AbstractMaker;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;

/**
 * @method string getCommandDescription()
 */
class TestMaker extends AbstractMaker
{

    public static function getCommandName(): string
    {
        // TODO: Implement getCommandName() method.
    }

    public function configureCommand(Command $command, InputConfiguration $inputConfig)
    {
        // TODO: Implement configureCommand() method.
    }

    public function configureDependencies(DependencyBuilder $dependencies)
    {
        // TODO: Implement configureDependencies() method.
    }

    public function generate(InputInterface $input, ConsoleStyle $io, Generator $generator)
    {
        // TODO: Implement generate() method.
    }

    public function __call($name, $arguments)
    {
        // TODO: Implement @method string getCommandDescription()
    }
}

I don’t work on Symfony core and there is no documentation so I can’t tell you what these methods do. However, you can look at all of the code for the makers and see what is done in them.

I do most of my work in the generate method. For getCommandDescription() method I do this

public static function getCommandDescription(): string
{
    return 'Clones a Doctrine Entity class.';
}

Additional info and links

I had to google and dig to find this  which explains the console command arguments and options.

Here is another link to the docs for the console commands you can use with Makers.

Here is yet another link to more information about the console and how to use it. This is to the console component itself. This link has most of the other helpful console related links to the bottom of the article. There looks to be about 20 links at this time. These are the most helpful links so I won’t repost every one of them here.

Here is yet another link about the console arguments and options.

And here is a link that explains the QuestionHelper class you use to ask questions.

Here is a link explaining the FormatHelper class used to format console output. This one is useful if you want to print messages in blocks with background colors.  If you want to change the color of the actual text you need to read this.

You can even display a clickable link to the user in the command line.

Categories
Software Development Web Development

Php how to use array_map with an anonymous function

In PHP you can use array_map() and the other array related methods like array_filter() with either a named function, an anonymous function, or an arrow function.
I didn’t see any examples in the PHP docs( they can be updated any day ) about how to use an anonymous function, so I tried it and here is how to do it. It does show how to use an arrow function which is a shorter way to accomplish this, but looks different. It is a preference thing, both work the same under the hood.

Below is the array_map function definition from the PHP docs.
array_map(?callable $callback, array $array, array ...$arrays): array
As you can see it takes a callable/callback function. This means you could create a function and supply it to this method. And that is a great way to do this… unless what you need to do in the function is very small.
For example I needed to remove the word “Repository” from every file name. I globbed the file names from the directory then I use this function


private function getRepositoryNames(): array{
$values = array_map(function ($val){
return str_replace('Repository', '', $val);
}, $this->_repositoryNames);

dump($values);
return $values;
}

Look closely and you will see the anonymous function. It is the first argument to the array_map() method. The second argument is the actual array I want to work on here. As you can see this would quickly become hard to read if the function were more than a few lines of code. You see this a lot more often in languages like Javascript and Scala.

As you can see I am simply using str_replace() If I needed to do much more it would be better to have a separate named function instead of an anonymous one for several reasons. The #1 reason to separate out the function is for readability purposes. The #2 I can think of relates to #1, maintaining or changing how the function works. But mostly if it is long separate it out so it is easier to read the code.

How does it work?

Basically the way array_map works is the second value you supply, an array, is looped over and supplied to your function one at a time and your function operates on it and returns it to array_map. When array_map is done looping over all the values it returns the new array. You can read more in the PHP docs here.

Links

A very good in depth article about using Php Arrow functions.

Php Anonymous Functions, what are they and why use them. – a very good in depth article about php anonymous functions. It is 10 years old, but still up to date and relevant.

Anonymous and Arrow functions in PHP – a very good article that covers both.

 

Categories
Software Development Web Development

My symfony command line cheat sheet

I can never remember all the symfony commands. So I will add them here one at a time as I need them to create a cheat sheet.

Cache – clearing the cache explained

Clear all the caches

php bin/console cache:pool:clear cache.global_clearer

 

Categories
Software Development Web Development

Symfony 5+ using the command line and listing commands

Symfony has useful command line commands. But what are the available commands and how do you get a list?

To get started type the following in the command line in your projects main/root folder. This is the directory that contains the directories src,assets, bin, public, node_modules etc.
php bin/console -help
That command will output the following

bin console help
Output of php bin/console -help command

This shows we can use bin/console list and a few more. Now try the command and see what the list shows.


Available commands:
  about                                      Displays information about the current project
  help                                       Displays help for a command
  list                                       Lists commands
 assets
  assets:install                             Installs bundles web assets under a public directory
 cache
  cache:clear                                Clears the cache
  cache:pool:clear                           Clears cache pools
  cache:pool:delete                          Deletes an item from a cache pool
  cache:pool:list                            List available cache pools
  cache:pool:prune                           Prunes cache pools
  cache:warmup                               Warms up an empty cache
 config
  config:dump-reference                      Dumps the default configuration for an extension
 dbal
  dbal:run-sql                               Executes arbitrary SQL directly from the command line.
 debug
  debug:autowiring                           Lists classes/interfaces you can use for autowiring
  debug:config                               Dumps the current configuration for an extension
  debug:container                            Displays current services for an application
  debug:event-dispatcher                     Displays configured listeners for an application
  debug:form                                 Displays form type information
  debug:router                               Displays current routes for an application
  debug:translation                          Displays translation messages information
  debug:twig                                 Shows a list of twig functions, filters, globals and tests
  debug:validator                            Displays validation constraints for classes
 doctrine
  doctrine:cache:clear-collection-region     Clear a second-level cache collection region
  doctrine:cache:clear-entity-region         Clear a second-level cache entity region
  doctrine:cache:clear-metadata              Clears all metadata cache for an entity manager
  doctrine:cache:clear-query                 Clears all query cache for an entity manager
  doctrine:cache:clear-query-region          Clear a second-level cache query region
  doctrine:cache:clear-result                Clears result cache for an entity manager
  doctrine:database:create                   Creates the configured database
  doctrine:database:drop                     Drops the configured database
  doctrine:database:import                   Import SQL file(s) directly to Database.
  doctrine:ensure-production-settings        Verify that Doctrine is properly configured for a production environment
  doctrine:fixtures:load                     Load data fixtures to your database
  doctrine:mapping:convert                   [orm:convert:mapping] Convert mapping information between supported formats
  doctrine:mapping:import                    Imports mapping information from an existing database
  doctrine:mapping:info                      
  doctrine:migrations:current                [current] Outputs the current version
  doctrine:migrations:diff                   [diff] Generate a migration by comparing your current database to your mapping information.
  doctrine:migrations:dump-schema            [dump-schema] Dump the schema for your database to a migration.
  doctrine:migrations:execute                [execute] Execute one or more migration versions up or down manually.
  doctrine:migrations:generate               [generate] Generate a blank migration class.
  doctrine:migrations:latest                 [latest] Outputs the latest version
  doctrine:migrations:list                   [list-migrations] Display a list of all available migrations and their status.
  doctrine:migrations:migrate                [migrate] Execute a migration to a specified version or the latest available version.
  doctrine:migrations:rollup                 [rollup] Rollup migrations by deleting all tracked versions and insert the one version that exists.
  doctrine:migrations:status                 [status] View the status of a set of migrations.
  doctrine:migrations:sync-metadata-storage  [sync-metadata-storage] Ensures that the metadata storage is at the latest version.
  doctrine:migrations:up-to-date             [up-to-date] Tells you if your schema is up-to-date.
  doctrine:migrations:version                [version] Manually add and delete migration versions from the version table.
  doctrine:query:dql                         Executes arbitrary DQL directly from the command line
  doctrine:query:sql                         Executes arbitrary SQL directly from the command line.
  doctrine:schema:create                     Executes (or dumps) the SQL needed to generate the database schema
  doctrine:schema:drop                       Executes (or dumps) the SQL needed to drop the current database schema
  doctrine:schema:update                     Executes (or dumps) the SQL needed to update the database schema to match the current mapping metadata
  doctrine:schema:validate                   Validate the mapping files
 fos
  fos:js-routing:debug                       Displays currently exposed routes for an application
  fos:js-routing:dump                        Dumps exposed routes to the filesystem
 lint
  lint:container                             Ensures that arguments injected into services match type declarations
  lint:twig                                  Lints a template and outputs encountered errors
  lint:xliff                                 Lints a XLIFF file and outputs encountered errors
  lint:yaml                                  Lints a file and outputs encountered errors
 make
  make:auth                                  Creates a Guard authenticator of different flavors
  make:command                               Creates a new console command class
  make:controller                            Creates a new controller class
  make:crud                                  Creates CRUD for Doctrine entity class
  make:docker:database                       Adds a database container to your docker-compose.yaml file
  make:entity                                Creates or updates a Doctrine entity class, and optionally an API Platform resource
  make:fixtures                              Creates a new class to load Doctrine fixtures
  make:form                                  Creates a new form class
  make:message                               Creates a new message and handler
  make:messenger-middleware                  Creates a new messenger middleware
  make:migration                             Creates a new migration based on database changes
  make:registration-form                     Creates a new registration form system
  make:reset-password                        Create controller, entity, and repositories for use with symfonycasts/reset-password-bundle
  make:serializer:encoder                    Creates a new serializer encoder class
  make:serializer:normalizer                 Creates a new serializer normalizer class
  make:subscriber                            Creates a new event subscriber class
  make:table-meta                            Creates classes with table data
  make:test                                  [make:unit-test|make:functional-test] Creates a new test class
  make:twig-extension                        Creates a new Twig extension class
  make:user                                  Creates a new security user class
  make:validator                             Creates a new validator and constraint class
  make:voter                                 Creates a new security voter class
 reset-password
  reset-password:remove-expired              Remove expired reset password requests from persistence.
 router
  router:match                               Helps debug routes by simulating a path info match
 secrets
  secrets:decrypt-to-local                   Decrypts all secrets and stores them in the local vault
  secrets:encrypt-from-local                 Encrypts all local secrets to the vault
  secrets:generate-keys                      Generates new encryption keys
  secrets:list                               Lists all secrets
  secrets:remove                             Removes a secret from the vault
  secrets:set                                Sets a secret in the vault security
  security:encode-password                   Encodes a password
  server
  server:dump                                Starts a dump server that collects and displays dumps in a single place
  server:log                                 Starts a log server that displays logs in real time
 translation
  translation:update                         Updates the translation file

Wow that is a lot of commands. Enter the list command in your terminal to get a better view.

mr rodgers how it is done meme
And that is how it is done!!!
Categories
Random Software Development

How to go back multiple directories in a linux terminal command line

Often when I am using the command line in a terminal on Linux I need to go back more than one directory. Usually you use the command

cd ..

Which takes you back one level in a directory. For example if you are in /var/www/http and you type cd .. you will be in directory /var/www/.

But what if you are super deep in the directory and need to get back to www directory. Say you are in /var/www/http/website/public and you want to get back to /var/www/http you can either type:
cd .. ( inside /var/www/http/website/ )
cd .. (inside /var/www/http/ now )

But you can type the following and get all the way back to the /www/http/ directory in one line
cd ../..
That line will take you back two directories. If you need to go further back just continue adding ../..

Categories
Software Development Web Development

How to fix Github git keeps asking for password with ssh keys

This was a super annoying issue I have had for years. I never looked into it because I was lazy.

I setup my ssh keys like you probably did and I kept getting prompted for my password.

I kept entering it for years because the github docs are not that great and fail to mention how to fully setup the keys properly(maybe changed by now). I kept wondering why it was asking me to enter my password even with the ssh keys.

Well a few days ago August 13th 2021, github switched from allowing password ssh git push/pull to not allowing it and forcing you to use something like ssh keys. I went to push to my github repo and was told I had to use ssh keys blah blah…

I was like

spock WTF
Wait. WTF?

I thought I had setup the ssh-keys. But, what had happened was I had used the HTTPS method to pull the repo. So my git config was set to the HTTPS endpoint not the SSH endpoint. It had been a long time ago. I thought I had setup the keys, and I did. What I failed to do was switch the git remote value in the git config.

are you kidding me
Freaking seriously

Basically if you pulled a repo via https then you need to switch your git configuration to use the ssh url. I won’t make this article longer writing how to do that, here is a great, short article that explains it

You also need to know the ssh repo value which you can find in the repository under clone like this

github clone
remote repo name

I have not tried the newer GitHub CLI. I will eventually read about it and try it and update my repos to use it. I did write about using github ssh deploy keys here though.  And about using multiple deploy ssh keys here.

Categories
Software Development Web Development

How to get URL Routes in your Javascript in Symfony 5+

The bottom of this article also explains how to update the routes. I’ll update this article as I find issues or things change. There are 85+ revisions so far. LOL

Do not miss the last step.

Because it prints the damn routes out after a user registers, no other time only after registration. I used my psychic abilities to solve this magical issue. LOL

Isn’t this exactly what you would expect???? Yeah me too.

In the Symfony docs it shows you how to get a URL to your javascript in your template. Basically what this is good for is it gives your Javascript code access to a url that you store in a javascript variable. You would place this at the bottom of your Twig template page, then you can access it with other Javascript code within that script tag area.

Another way to do it is to hide an element with an id in the template then use Javascript to fetch it.

Another way to get the routes is also mentioned in the Symfony Documentation, FosJsRoutingBundle. This allows you to skip the other ways and be able to just get the routes in your Javascript. For example if your frontend is React, Vue etc. or you have a Javascript component that needs to use AJAX to interact with the backend.

The docs say something about app/appKernel.php that file no longer exists.  It has been replaced with just kernel.php and you do pretty much nothing to that file. What you do need to do is make sure that you add this line or it exists, the bundle system should add it.

This goes inside (should be inside) app\config\bundles.php file, you will see a list of the bundles your app is using in this file too.

FOS\JsRoutingBundle\FOSJsRoutingBundle::class => ['all' => true],

I am not sure what version Symfony started this but I know in 5+ you don’t have to register the bundle like step 2 of the old docs shows. This is done automatically for you in a new file named config/bundles.php which is where all bundles are automatically registered when you install them.

So all you do is this command now in the terminal

composer require friendsofsymfony/jsrouting-bundle

inside your apps root level. It is auto registered for you and you will see it listed inside composer.json file too.

You don’t have to do step 3 of the old docs either as the config system has changed. There is no file anymore. When you install the bundle the new system creates a new file named app/config/routes/fos_js_routing.yaml for you and the required code is inside. NICE!!!

borat nice meme
Very nice!!!

For step 4 the required code is the same for version 5 as for version 4. I don’t know what this actually does.

$ php bin/console assets:install --symlink public

But I know, you will also need to use this command or nothing will work. This is also how you update( see below)

bin/console fos:js-routing:dump --format=json --target=public/js/fos_js_routes.json

That actually puts the file with the routes in the proper location so you can use it

Basically to install the bundle you just use the composer command and most of the stuff is now done for you.

Don’t forget to expose your routes

expose routes meme
Don’t forget to expose your routes.

I told you there are many baby steps.

In order for the routes to work you must add something new to every single route definition that you want to be able to generate a route for in your Javascript.

You must add this to the annotation or where ever you define your routes. I use annotations so I can just look to the controllers.

options={"expose"=true}

So for my menu route the definition in the controller looks like this.

@Route("/menu", name="menu", options={"expose"=true}, methods={"GET"})
</code.

Now how to use it?

yoda use the routes meme
Use the routes Luke.

So now that it is installed how do you use it? One more step. Now you must include the needed Javascript in your page with these tags. If you are not using Webpack with Symfony you can use these lines.

<script src="{{ asset('bundles/fosjsrouting/js/router.js') }}"></script>
<script src="{{ path('fos_js_routing_js', { callback: 'fos.Router.setData' }) }}"></script>

Place those anywhere you want.

If you are using Webpack with Symfony these lines are easier. Place these at the top of the file where you need to access routes.

const routes = require('../../public/js/fos_js_routes.json');
import Routing from '../../vendor/friendsofsymfony/jsrouting-bundle/Resources/public/js/router.min.js';

What I did is put them in my base twig template in a section where I include my Javascript this way it is available on every page because I will probably be needing access to routes as I build the app.

If you don’t know, if you are using webpack then there will be an app.js file in your app\assets\js\app.js

Learn how to install webpack here in the Symfony Docs. Learn more about how to use Webpack with Symfony here.

You include this in your base template. Then anything you add to it will be included for other Javascript code you use with import statements. You must be using Webpack for this to work though. This is where I have included JQuery and Bootstrap and now FosJsRouting for example.

Once you have all of the above done you can use it like this


//set the routes for the Routing object
Routing.setRoutingData(routes);
//create a url
let testUrl = Routing.generate( 'menu');
//check to see it is working
console.log(testUrl); //outputs /menu

Read a lot more about generating URLs in the FosJsRouting documentation.

Route not found errors?

more errors meme
And.. Errors

Yes I got these too. You must call this setRoutingData() method first with the routes constant created above. I was like WTF too.

Routing.setRoutingData(routes);

The last step

What ever you do, do not forget this crucial step. With the standard Symfony Authentication system created with the Maker bundles, your users will be returned to the page they last viewed when they login/register.

There are more ways to do this than the one I suggest. But I suggest using a Subscriber class and extending

EventSubscriberInterface

The reason is, this works for both the Login and Registration forms and you don’t have to repeat code anywhere. Below is what my class looks like.


namespace App\EventSubscriber;


use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpFoundation\Session\SessionInterface;
use Symfony\Component\HttpKernel\Event\RequestEvent;
use Symfony\Component\HttpKernel\KernelEvents;
use Symfony\Component\Security\Http\Util\TargetPathTrait;

class TargetPathSubscriber implements EventSubscriberInterface
{

    use TargetPathTrait;

    private SessionInterface $session;

    public function __construct(SessionInterface $session)
    {
        $this->session = $session;
    }

    public function onKernelRequest(RequestEvent $event): void
    {

        $request = $event->getRequest();
        //don't save the path if the user has come to the login page first making a
        //never ending loop. Dont save AJAX requests either. Make sure this is a master
        //not a sub request, subs happen in forms
        $route = $request->attributes->get('_route');
        dump('Route is ' . $route);

        if (
            !$event->isMasterRequest()
            || $request->isXmlHttpRequest()
            || 'app_login' === $route
            || 'app_register' === $route
            || 'js/routing?callback=fos.Router.setData' === $route
            || 'menu'
            || 'alias_exists'
            || 'email_exists'
        ) {
            return;
        }

        $this->saveTargetPath($this->session, 'main', $request->getUri());

    }

    public static function getSubscribedEvents(): array
    {
        return [
            KernelEvents::REQUEST => ['onKernelRequest']
        ];
    }
}

You can read more about how this works in the Symfony documentation here. What the code inside onKernelRequest() does is it saves the targetPath aka the last page the user was viewing/tried to access. The problem is it saves things you don’t want like the login or registration routes.
What these lines of code are doing is excluding routes. What the Authentication system does is it looks for the _targetpath. You will need to make changes to your firewall too in order for this to work. More on that here.

Basically the code above only sets a targetpath if it is not one of the routes in the list. I had to add '/js/routing...' === $route so that the user would not be redirected to that, which is a script which outputs all of your routes you have exposed to FosJsRoutingBundle.

I set my firewall so that if nothing exists for targetpath, which will be the case for the routes in the list or if a user goes directly to yoursite.com What we need in that instance is a default page to redirect the user to.

This is what my firewall code looks like (inside config/packages/security.yaml


firewalls:
    dev:
      pattern: ^/(_(profiler|wdt)|css|images|js)/
      security: false
    main:
      lazy: true
      entry_point: form_login
      provider: app_user_provider
      form_login:
        login_path: app_login
        check_path: app_login
        #csrf_token_generator: security.csrf.token_manager
        enable_csrf: true
        default_target_path: list_articles
        username_parameter : 'email'
        password_parameter: 'password'

See this one magic line ( default_target_path: list_articles ) if nothing is set in my TargetPathSubscriber this is the route that is used.

The interesting and most confusing part of all of this was the fact that Symfony only displayed the FosJsRoutingBundle routes when a user registered not when they logged in. That was insanely confusing.

so much work meme
I told you getting routes in your symfony js is a lot of work

Updating your routes

Every time you add a new routes or change a routes name you must update the routes.json file or else you will get route not found errors. To update your routes use this command.

php bin/console fos:js-routing:dump --format=json --target=public/js/fos_js_routes.json
And now you know how to get routes in your Javascript in Symfony

View all the routes

You may also need or want to view which routes are now exposed after updating or out of curiosity. The following command will output all of your exposed routes.

 php bin/console fos:js-routing:debug

Links

More on stackoverflow about redirecting after registration.

An older SymfonyCast about integrating FosJsBundle

Here is the link to the FosJsRoutingBundle installation documentation on github which this blog basically explains.

Events and EventListeners in the Symfony Documentation.

More about using targetpath in this SymfonyCast

Here is a link to my favorite HTMLEntities converter if you write articles about programming you will need this.

 

Categories
Software Development Web Development

How to create and use a custom Javascript Event

You have probably used events in Javascript many times.  Especially if you have done any User Interface programming. There are many types of events provided by browsers and the Javascript engines.

Did you know you can create your own custom events with the CustomEvent() constructor? Here is a minimal example. In this article I will explain custom events and how to use them.

First off what would you use a custom event for? You use them to notify other objects in your app that actions have occurred such as “user clicked x” or “user closed dialog”. Using events prevents code coupling and reduces dependencies.

I came across this need when creating a dialog box where I wanted an overlay to show beneath it blocking out the page behind. I didn’t want my DialogBox to have to know about my Overlay.  I didn’t want them coupled. I didn’t want my DialogBox to have to have an Overlay object as an argument creating a dependency. I didn’t want my DialogBox to even know that an Overlay object existed.

The answer is for my DialogBox to emit/create/dispatch a custom Javascript Event and have a listener for that event to close the Overlay.

So lets look at some code.


import {Utils} from "./Utils";

class DialogBox {

    /**
     *
     * @param {string} dialogId
     * @param {string} dialogClass
     */
    constructor(dialogId = 'dialogId', dialogClass = '') {
        this.closeDialogId = 'closeDialog';
        this.dialogBoxBottomRowId = 'dialogBoxBottomRow';
        this.dialogBoxClass = 'dialogBox ' + dialogClass;
        this.contentContainerId = 'dialogContentContainer';
        this.dialogHtml = '';
        this.dialogBoxId = dialogId;
        this.closeHandler = null;
        this.divElement = null;
    }

    /**
     *
     * @param {string} rowContent : the content HTML etc to be added as a row
     * @param {string} cssClass : applied only if passed in
     * @returns {void}
     */
    bottomRow(rowContent = '', cssClass = 'dialog-bottom-row') {

        let bottomRow = document.getElementById(this.dialogBoxBottomRowId);

        //if the bottom row does not exist add it to the html
        if ( Utils.isEmpty(bottomRow)) {
            let rowHtml = '<div id="' + this.dialogBoxBottomRowId + '" ';
            rowHtml += ' class="' + cssClass + '" ';
            rowHtml += ' >' + rowContent + '</div>';
            this.dialogHtml += rowHtml;
        } else {
            //if a bottom row exists replace it
            bottomRow.className = cssClass;
            bottomRow.innerText = rowContent;
        }
    }
    /**
     * Centers the dialog vertically and horizontally in the parent element
     * @param {string} parentElementId
     */
    centerDialog(parentElementId = 'body') {

        let parentWidth = 0;
        let parentHeight = 0;
        //need the dialog boxes calculated width and height
        let dialogHeight = this.divElement.clientHeight;
        let dialogWidth = this.divElement.clientWidth;

        //must use two different ways to get the height and width
        if (parentElementId === 'body') {
            parentWidth = window.innerWidth;
            parentHeight = window.innerHeight;
        } else {
            let parentElement = document.getElementById(parentElementId);
            //make sure null or undefined were not returned
            if (!Utils.isEmpty(parentElement)) {
                parentHeight = parentElement.clientHeight;
                parentWidth = parentElement.clientWidth;
            }
        }
        let left = (parentWidth / 2) - (dialogWidth / 2);
        let top = (parentHeight / 2) - (dialogHeight / 2);
        //must add px or it doesn't work at all
        this.divElement.style.top = top + 'px';
        this.divElement.style.left = left + 'px';
    }
    /**
     *
     * @param {string} content
     * @param {string} containerClass
     */
    contentContainer(content, containerClass = 'dialog-content') {
        let contentDiv = document.getElementById(this.contentContainerId);

        //if the container exists replace it contents
        if (Utils.isEmpty(contentDiv)) {
            let contentHtml = '<div id="' + this.contentContainerId + '"';
            contentHtml += ' class="' + containerClass + '" >';
            contentHtml += content + '</div>';
            this.dialogHtml += contentHtml;
        } else {
            contentDiv.innerHTML = content;
            contentDiv.className = containerClass;
        }
    }
    /**
     * calls removeDialogBox which removes the dialog and event listeners
     */
    hideDialogBox() {
        this.removeDialogBox();
    }

    /**
     *
     * @param {string} menuText
     * @param {string} menuTextClass
     */
    menuBar(menuText, menuTextClass = '') {
        let menuTextId = 'dialog-menu-text';
        /*
         * if dialogMenuBar is present then the length will be non zero or true
         * if this is the case replace the content, this allows this method to be called
         * again later to change the value
         */
        let menuBarID = 'dialog-menu-bar';
        let menuTextDiv = document.getElementById(menuTextId);

        if (Utils.isEmpty(menuTextDiv)) {
            this.dialogHtml = '<div class="dialog-menu-bar" id="dialog-menu-bar" >';
            this.dialogHtml += '<div id="' + menuTextId + '" class="dialog-menu-text ';
            this.dialogHtml += menuTextClass + '" >' + menuText + '</div>';
            this.dialogHtml += '<div id="' + this.closeDialogId + '" class="close-dialog" >';
            this.dialogHtml += '<img src="/images/drawing/close-window.png" ';
            this.dialogHtml += 'alt="Close dialog" >';
            this.dialogHtml += '</div></div>';
        } else {
            menuTextDiv.innerText = menuText;
            menuTextDiv.className = menuTextClass;
        }
    }

    /**
     * displays the dialog box, you must call centerDialog to center it
     */
    showDialogBox() {
        //remove any existing dialog boxes first
        this.removeDialogBox();
        this.divElement = document.createElement("div");
        this.divElement.id = this.dialogBoxId;
        this.divElement.className = this.dialogBoxClass;
        this.divElement.innerHTML = this.dialogHtml;
        //position the dialog box now give it the highest z-index to be on top
        this.divElement.style.zIndex = Utils.getHighestZIndex() + 1;
        document.body.appendChild(this.divElement);
        //add the listener for when the user clicks to close

        this.closeHandler = function ( ) {
        this.removeDialogBox();
        }.bind(this);

        let close = document.getElementById(this.closeDialogId);
        close.addEventListener('click', this.closeHandler, false);
    }

    /**
     * removes the dialog html from the page and removes the close listener
     * dispatches event 'dialogClosed' to be used to close an overlay etc.
     */
    removeDialogBox() {
        let dialogElem = document.getElementById(this.dialogBoxId);
        //if a dialog box of the same id exists delete it first to prevent errors and issues
        if (dialogElem) {
            let close = document.getElementById(this.closeDialogId);
            close.removeEventListener('click', this.closeHandler, false);
            dialogElem.remove();
            const dialogEvent = new CustomEvent('dialogClosed');
            document.body.dispatchEvent(dialogEvent);
        }
    }
}

export {DialogBox}

That is a lot of code 169 lines to be exact. I am still in the process of converting this code, still going to add some Template literals instead of the old fashioned string concatenation technique.

There are several very import things to note here in this code. For example the way the addEventListener() and removeEventListener are used. These functions have to be passed THE EXACT SAME parameters or removeEventListener() fails to remove the event listener and that clutters your memory up because you will have listeners referring to elements that don’t exist.

That is why I have this code this.closeHandler


//add the listener for when the user clicks to close

        this.closeHandler = function ( ) {
        this.removeDialogBox();
        }.bind(this);

        let close = document.getElementById(this.closeDialogId);
        close.addEventListener('click', this.closeHandler, false);

See this.closeHandler = function  that stores the function to handle the click on the close button. Both the add and remove event listener functions have to be passed the exact same function.

Look at the removeDialogBox function closer.


let dialogElem = document.getElementById(this.dialogBoxId);
        //if a dialog box of the same id exists delete it first to prevent errors and issues
        if (dialogElem) {
            let close = document.getElementById(this.closeDialogId);
            close.removeEventListener('click', this.closeHandler, false);
            dialogElem.remove();
            const dialogEvent = new CustomEvent('dialogClosed');
            document.body.dispatchEvent(dialogEvent);
        }

Notice that the second argument to removeEventListener is this.closeHandler that is the same function passed to the addEventListener above.

If you use anonymous functions inside add and remove event listeners instead, then they wont be the same function and so your event listener won’t be removed and your memory fills up faster.

Another important note is that the this.handler function must use bind(this) like so

this.closeHandler = function ( ) { this.removeDialogBox(); }.bind(this);

If you don’t bind the function expression then you will get an error about this.removeDialogBox is not a function.
This is because you are storing the closeHandler in memory for later use.  At that later time the context will be different, the code won’t be executing within your class anymore, it will be in it’s own context. That means “this” that was alive in your class, no longer exists. Which means that function no longer exists You must bind “this” by using “.bind(this) at the end of the function.

And now about the Custom Event. You will see it at the bottom of the removeDialogbox() function

const dialogEvent = new CustomEvent('dialogClosed');
document.body.dispatchEvent(dialogEvent);

Those two lines is all it takes to create and dispatch your own Custom event.

Listening for custom events

This means you write code that listens for the custom “dialogClosed” event to be fired like this.

document.body.addEventListener('dialogClosed', function (){
overlay.hideOverlay();
});

Notice I am using document.body this is a very easy way to create the listener. This is using an anonymous function which is bad because this listener can’t be removed. It should be removed right below this. In order to do that you would need to create the handler function above it and pass it to both the add and remove event listeners.

Adding information to the event.

This is one of the most handy parts of custom events, the ability to pass information in the event. This can be any information, a full object even.
In this article it mentions adding custom data with “detail”.  Here is an example from the code above.

const dialogEvent = new CustomEvent('dialogClosed',{ detail: {
        id: this.dialogBoxId
    } });

Here I am passing id you can use this same format to pass many more values just add a comma to the end of each one. Then to access the extra information in your listener you do like this.

document.body.addEventListener( 'dialogClosed', function (event) {

let dialogId = event.detail.id
switch (dialogId) {
case mainDialogId :
mainOverlay.hideOverlay();
break;
case colorDialogId :
overlay2.hideOverlay();
break;
}

}, false);

More about addEventListener

Above I am using event.detail.id to get the value I stored in id in the detail of the custom event. Notice how I am using a switch statement to compare the id’s of the dialog that closed to close the correct overlay. There is no default behavior for this action, either one dialog closes or another. I could have 5 different dialogs if I wanted.

Links

The living DOM specification – covers javascript events and listeners and custom events etc. all in great detail. This is the language specification ie. “How things should work” Each browser implements it’s own version of this.

MDN addEventListener – great information about adding event listeners with lots of examples.

MDN creating and triggering events – great article about how to create and trigger custom events with examples.

MDN CustomEvent – basic documentation and good information about the CustomEvent object.

Categories
Software Development Web Development Web Security

Authentication vs Authorization what is the difference?

Authentication/Authorization these terms are often confused. Here I will clarify them.

Authentication — Login, proving who a user is one way or another. After a user is logged into a system a session cookie is usually created to re-authenticate the user so they don’t have to login every single page view.

Authorization — Can a user view or access something once Authenticated? Authorization includes things like administration panel access, viewing a users profile or post or media etc.

Categories
Resources Software Development

Ethereum Solidity programming links and resources.

Links and resources about Ethereum Solidity programming language.

Videos