Question

I have the following tables in pgsql 8.4:

user
permission
role
user_permission (n:m)
role_permission (n:m)
user_role (n:1)

I have a REST service, by GET /user/1 I have to get something like this from the database:

{
    id:1,
    name: "John Smith",
    roles: [
        {id: 1, name: "Administrator"},
        {id: 2, name: "Editor"}
    ],
    permissions: [
        {id: 1, method: "GET", resource: "^/$"}
    ]
}

Where roles are from tables role & user_role and permissions are from tables permission & user_permission. By PUT /user/1 I want to send similar data to the database.

Is it possible to do that with pl/pgsql functions, or I have to write simple queries, and execute them one-by-one from php?

I don't necessarily want to send json to the pgsql, if this is possible with php arrays without concatenated strings, etc..., it would be more than enough.

I can't upgrade the server to a higher version.

Relational mapping is not good for me, I use stored procedures for everything because I don't want to write sql templates in php. I need just raw data, the only thing I do with that is json_encode or json_decode, so the classic ORM way is useless here I think because I have the business logic in pl/pgsql functions in the database, and php just validates and converts messages.

Was it helpful?

Solution 2

It is possible: "Returning a nested composite type from a PL/pgSQL function", but since there is no built-in associative array in pgsql, it is way too hard to do the data conversion to php associative arrays and json after that. So I think the best solution here to read the data with multiple queries and build the associative array with php.

In pgsql 9.2 it would be easier to do this with json data type and a custom setof record -> json aggregate function. In 8.4 I don't think it is worth the effort...

OTHER TIPS

Be aware user and role are database reserved words.

Just ask the database what you want with one query:

SELECT
    my_user.id
    my_user.name,
    array_agg(my_role) AS "roles",
    array_agg(permission) AS "permissions"
FROM
  "user" my_user
    LEFT JOIN user_permission up ON my_user.id = up.user_id
    LEFT JOIN permission ON up.permission_id = permission.id
    LEFT JOIN user_role ur ON user.id = ur.user_id
    LEFT JOIN "role" my_role ON my_role.id = ur.role_id

And you will get an ugly string with dobule escaped objects into array. Using Pomm layer will allow you to use the converter to get arrays of PHP objects directly from this query.

<?php

public function getUserPermissionsAndRoles($user_id)
{
    $sql = <<<SQL
SELECT
    :user_fields_as_my_user,
    array_agg(my_role) AS "roles",
    array_agg(permission) AS "permissions"
FROM
  ":user_table" my_user
    LEFT JOIN :user_permission_table up ON my_user.id = up.user_id
    LEFT JOIN :permission_table ON up.permission_id = permission.id
    LEFT JOIN :user_role_table ur ON user.id = ur.user_id
    LEFT JOIN ":role_table" my_role ON my_role.id = ur.role_id
WHERE
  my_user.id = ?
SQL;

    $sql = strtr($sql, array(
        ":user_fields_as_my_user" => $this->formatFieldsWithAlias('getSelectFields', 'my_user'),
        ":user_table" => $this->getTableName(),
        ":user_permission_table" => $this->getConnection()->getMapFor("\Db\App\UserPermission")->getTableName(),
        ":permission_table" => $this->getConnection()->getMapFor("\Db\App\Permission")->getTableName(),
        ":user_role_table" => $this->getConnection()->getMapFor("\Db\App\UserRole")->getTableName(),
        ":role_table" => $this->getConnection()->getMapFor("\Db\App\Role")->getTableName(),
        ));

    return $this->query($sql, array($user_id));
}

In order to tell Pomm it can be converter of user_role, user_permission, permission and role database objects, when instanciating your database, add the following :

$database = new \Pomm\Database(array("dsn" => "pgsql://user:pass@host:port/db_name", "name" => "app"));

$cnct = $database->getConnection();

$database
    ->registerConverter('UserPermission', new \Pomm\Converter\PgEntity($cnct->getMapFor('\Db\App\UserPermission')), array('user_permission'))
    ->registerConverter('Permission', new \Pomm\Converter\PgEntity($cnct->getMapFor('\Db\App\Permission')), array('permission'))
    ->registerConverter('UserRole', new \Pomm\Converter\PgEntity($cnct->getMapFor('\Db\App\UserRole')), array('user_role'))
    ->registerConverter('Role', new \Pomm\Converter\PgEntity($cnct->getMapFor('\Db\App\Role')), array('role'))
    ;

Now the database knows theses converters can be used but the UserMap has to know when it parses a result field named "roles" and "permissions", it must handle them as an array of Role and Permission entities. This is what the initialize() function of the UserMap class stands for:

<?php

class UserMap extends \Db\App\Base\UserMap
{
    public function initialize()
    {
        parent::initialize();

        $this->addVirtualField('roles', 'Role[]');
        $this->addVirtualField('permissions', 'Permission[]');
    }

Now, in your controller you just have to call the model's method and cast it to a JSON response:

    <?php

    $database = new \Pomm\Database(array("dsn" => "pgsql://user:pass@host:port/db_name", "name" => "app"));

    $cnct = $database->getConnection();

    $database
        ->registerConverter('UserPermission', new \Pomm\Converter\PgEntity($cnct->getMapFor('\Db\App\UserPermission')), array('user_permission'))
        ->registerConverter('Permission', new \Pomm\Converter\PgEntity($cnct->getMapFor('\Db\App\Permission')), array('permission'))
        ->registerConverter('UserRole', new \Pomm\Converter\PgEntity($cnct->getMapFor('\Db\App\UserRole')), array('user_role'))
        ->registerConverter('Role', new \Pomm\Converter\PgEntity($cnct->getMapFor('\Db\App\Role')), array('role'))
        ;

    $collection = $cnct->getMapFor('\Db\App\User')
        ->getUserPermissionsAndRoles($_GET['user_id']) // <- get an iterator over fetched users with extra info.
        ;

    echo json_encode($collection->extract()); // <- dump an array from the iterator and cast it to json. 

Of course the pure PHP part would be a way easier with a micro-framework like Silex where your controller would be:

<?php //...

$app->get('/user/{user_id}', function($user_id) use ($app) {
    $collection = $app['pomm.connection']
        ->getMapFor('\Db\App\User')
        ->getUserPermissionsAndRoles($user_id);

    if ($collection->count() === 0)
    {
        $this->abort(404, sprintf("No such user '%s'.", $user_id));
    }

    return $app->json($collection->extract());
});

Cheers !

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top