Category: "Symfony"

mod_proxy_ajp declining URL

Many hours of searching and suffering were spent trying to resolve this ...

Apache-Error: [file "mod_proxy_ajp.c"] [line 743] [level 7] AH00894: declining URL fcgi://localhost/var/www/html/site/public/index.php

This is a Symfony application (ibexa DXP, formerly eZ Platform, previously eZ Publish), CentOS 8 server, with PHP 7.4, mod_security, selinux enabled, etc.

The error was thrown on the graphql requests to support the sub-items display of the admin interface.

The root cause of the issue was an application error.

The message from Symfony

[2022-01-05T18:34:58.709826+00:00] request.CRITICAL: Uncaught PHP Exception Overblog\GraphQLBundle\Resolver\UnresolvableException: "Could not found type with alias "RepositoryLanguage". Do you forget to define it?" at /var/www/html/site/vendor/overblog/graphql-bundle/src/Resolver/TypeResolver.php line 72 {"exception":"[object] (Overblog\\GraphQLBundle\\Resolver\\UnresolvableException(code: 0): Could not found type with alias \"RepositoryLanguage\". Do you forget to define it? at /var/www/html/site/vendor/overblog/graphql-bundle/src/Resolver/TypeResolver.php:72)"} []

This also took me a long time to unravel - about an hour ... because I haven't worked with eZ in a while

Solution was to copy all these files https://github.com/bgamrat/improved-journey/tree/main/config/graphql/types/ezplatform into the config.

Next, it was time to make this blog post with the goal of helping you!

I changed the username for security, to a word I rarely use. So of course I forgot it. As well as the password. Tried to email a password reset, but that failed too ... selinux, remember?

setsebool -P httpd_can_sendmail 1

I also tried to reset the password at the database level, but that looked like more effort.

So - it was a grand adventure, the installation works and I can go do other things.

Returning custom headers with FOSRestBundle

The Content-Range header supports a (dgrid) OnDemandGrid

Returning custom headers with FOSRestBundle


use FOS\RestBundle\View\View as FOSRestView;
...
$view = FOSRestView::create();
$view->setData($data);
$view->setHeader( 'Content-Range', 'items '.$offset.'-'.($offset+$limit).'/'.$count);
$handler = $this->get('fos_rest.view_handler');
return $handler->handle($view);

Ref: https://symfony.com/doc/1.5/bundles/FOSRestBundle/2-the-view-layer.html

Symfony 4

Symfony 4 - Multiple DataFixtures files

I recently upgraded a Symfony 3.3 application to Symfony 4

Part of the upgrade was loading the DataFixtures.

Symfony 4 recommends you put all your DataFixtures in a single file. I'll get around to that later. However, due to the way I organized the file system for the project, the Doctrine Fixtures Loader could not find the demo data.

Symfony 4 - Multiple DataFixtures files

To resolve the issue, I created a services_dev.yaml file with the following:

services:
    App\DataFixtures\Demo\:
        resource: '../src/DataFixtures/Demo/*'
        tags: [ doctrine.fixture.orm ]

Once I added this file to the development server, the data loaded fine.

Ref: https://symfony.com/doc/master/bundles/DoctrineFixturesBundle/index.html#multiple-files

Upgrading from Symfony 3 to 4 - JSON Database Content

My latest adventure has been to upgrade a web application from Symfony 3.3 to 4. All the pages load and I am starting to test execution.

This error came up and I scoured the code for instances of AppBundle

Upgrading from Symfony 3 to 4 - JSON Database Content

Then I checked the database.

One of the attributes is custom_attributes, which is a JSON column. Sample content:

[{"#type":"AppBundle\\Entity\\CustomAttribute","key":"expiration","value":"2018-02-26","valueValidExpiration":true,"valueValidChannels":true},{"#type":"AppBundle\\Entity\
\CustomAttribute","key":"channels","value":6,"valueValidExpiration":true,"valueValidChannels":true}]

I am using https://github.com/dunglas/doctrine-json-odm to provide JSON data within entities.

To change AppBundle to App, I used:

dev=# UPDATE asset SET custom_attributes= REPLACE(custom_attributes::TEXT,'AppBundle','App')::json;
UPDATE 25
dev=# UPDATE model SET custom_attributes= REPLACE(custom_attributes::TEXT,'AppBundle','App')::json;
UPDATE 9

Revised Dijit/Tree Menu for Symfony

This is the final implementation for the tree menu I'm using for a Symfony application.

The first approach wrote the JSON for the menu store into the template. This was a little awkward and as the application grew, the amount of content increased.

Next, I had an idea that I could create a multi-tiered tree which would have a static top tier, but include content in the lower levels. The value of this was that the user could navigate directly to content, for example the details of a client. Lazy loading was used to limit the number of requests and size of responses. It worked. But it was slow. And I realized that as the amount of data increased, it would get slower. In addition, navigating through three or more levels on a tree is inefficient. So, I wanted something that was faster and streamlined.

Revised Dijit/Tree Menu for Symfony

The first change I made was to add an autocomplete search box, which allows the user enter the name of the item they need. It could be the name of a person, a barcode or a company. The search code isn't complete.

Then I rewrote the JsonRenderer and the MenuStoreController such that there is only a static menu delivered via Ajax.

services.yml

These are the service configurations.


    app.admin.menu_store_controller:
        class: AppBundle\Controller\Api\Admin\Common\MenuStoreController

    app.menu_renderer:
        class: AppBundle\Menu\JsonRenderer
        arguments: [ "@twig",  "knp_menu.html.twig", "@knp_menu.matcher", {"translator": "@translator" }]
        tags:
            - { name: knp_menu.renderer, alias: json }

MenuStoreController

The MenuStoreController calls the KnpMenuBuilder, then the JsonRenderer and delivers the result as JSON to the client.


<?php

namespace AppBundle\Controller\Api\Admin\Common;

use FOS\RestBundle\Controller\FOSRestController;
use Symfony\Component\HttpFoundation\Request;
use FOS\RestBundle\Controller\Annotations\View;

class MenuStoreController extends FOSRestController
{
    /**
     * @View()
     */
    public function getAdminmenuAction( Request $request )
    {
        $this->denyAccessUnlessGranted( 'ROLE_ADMIN', null, 'Unable to access this page!' );
        $adminMenu = $this->get( 'app.menu_builder' )->createAdminMenu( [] );
        $renderer = $this->get( 'app.menu_renderer' );
        return array_values( $renderer->render( $adminMenu ) );
    }

}

JsonRenderer

The JsonRenderer iterates through the menu and creates the data for the dijit/Tree.

<?php

namespace AppBundle\Menu;

use Knp\Menu\ItemInterface;
use Knp\Menu\Matcher\MatcherInterface;
use Knp\Menu\Renderer\RendererInterface;

class JsonRenderer implements RendererInterface
{

    /**
     * @var \Twig_Environment
     */
    private $environment;
    private $matcher;
    private $defaultOptions;
    private $translator;

    /**
     * @param \Twig_Environment $environment
     * @param string            $template
     * @param MatcherInterface  $matcher
     * @param array             $defaultOptions
     */
    public function __construct( \Twig_Environment $environment, $template, MatcherInterface $matcher, array $defaultOptions = array() )
    {
        $this->environment = $environment;
        $this->matcher = $matcher;
        $this->defaultOptions = array_merge( array(
            'depth' => null,
            'matchingDepth' => null,
            'currentAsLink' => true,
            'currentClass' => 'current',
            'ancestorClass' => 'current_ancestor',
            'firstClass' => 'first',
            'lastClass' => 'last',
            'template' => $template,
            'compressed' => false,
            'allow_safe_labels' => false,
            'clear_matcher' => true,
            'leaf_class' => null,
            'branch_class' => null
                ), $defaultOptions );
    }

    public function render( ItemInterface $item, array $options = array() )
    {
        $options = array_merge( $this->defaultOptions, $options );
        if( empty( $options['depth'] ) )
        {
            $options['depth'] = PHP_INT_MAX;
        }

        $this->translator = $options['translator'];

        $itemIterator = new \Knp\Menu\Iterator\RecursiveItemIterator( $item );

        $iterator = new \RecursiveIteratorIterator( $itemIterator, \RecursiveIteratorIterator::SELF_FIRST );

        $tree = [];
        $parent = null;
        $levelParent[0] = null;

        $lastLevel = null;
        foreach( $iterator as $item )
        {
            $translatedLabel = $this->translator->trans( $item->getLabel() );
            $id = strtolower( $item->getName() );
            $level = $item->getLevel();
            if( $level <= $options['depth'] )
            {
                $node = [];
                $node['id'] = $id;
                $node['name'] = $translatedLabel;
                $node['uri'] = $item->getUri();
                $node['has_children'] = $item->hasChildren();
                $node['level'] = $level;
                if( $lastLevel !== null )
                {
                    if( $level > $lastLevel )
                    {
                        $parent = $levelParent[$level] = $lastNode['id'];
                    }
                    else
                    {
                        if( $level < $lastLevel )
                        {
                            $parent = $levelParent[$level];
                        }
                    }
                    $lastParent = $parent;
                }
                $node['parent'] = $parent;
                $tree[$id] = $node;
                $lastLevel = $level;
                $lastNode = $node;
            }
        }
        return $tree;
    }

}

menu.js

menu.js requests the menu data and renders it on the page.

define([
    "dojo/dom",
    "dojo/request/xhr",
    "dojo/store/Memory",
    "dijit/tree/ObjectStoreModel",
    "dijit/Tree",
    "dojo/store/JsonRest",
    "dijit/form/ComboBox",
    "dojo/i18n!app/nls/core",
    "dojo/domReady!"
], function (dom,
        xhr, Memory, ObjectStoreModel, Tree, JsonRest, ComboBox, core) {
//"use strict";
    function run() {

        var searchInput, searchStore;

        xhr.get("/api/menustore/adminmenus/", {
            handleAs: "json"
        }).then(function (res) {
            var i, l, store = [], memory, model;
            l = res.length;
            for( i = 0; i < l; i++ ) {
                store.push(res[i]);
            }
            memory = new Memory({
                data: store,
                getChildren: function (object) {
                    return this.query({parent: object.id});
                }
            });

            // Create the model
            var model = new ObjectStoreModel({
                store: memory,
                query: {id: 'admin'},
                mayHaveChildren: function(object) {
                    return typeof object.has_children !== "undefined" && object.has_children;
                }
            });

            // Create the Tree.
            var tree = new Tree({
                id: "admin-menu",
                model: model,
                persist: true,
                showRoot: false,
                onClick: function (item) {
                    if( typeof item.uri !== "undefined" && item.uri !== null ) {
                        location.href = item.uri;
                    }
                },
                getIconClass: function (item, opened) {
                    return (item && item.has_children) ? (opened ? "dijitFolderOpened" : "dijitFolderClosed") : "dijitLeaf"
                }
            }, "admin-left-menu");
            tree.startup();
        });

        searchStore = new JsonRest({
            target: '/api/store/search',
            useRangeHeaders: false,
            idProperty: 'id'});
        searchInput = new ComboBox({
            trim: true,
            "class": "search",
            store: searchStore,
            searchAttr: "name",
            placeholder: core.search
        }, "search");
        searchInput.startup();
        searchInput.on("change", function (evt) {
            console.log(evt);
            console.log(searchInput.store);
        });

    }
    return {
        run: run
    };
});
//# sourceURL=menu.js

1 3