charlesportwoodii / yii2-psr7-bridge

A PSR7 Bridge and PSR-15 adapter for Yii2
BSD 3-Clause "New" or "Revised" License
110 stars 20 forks source link

URLManager Parameters not available in yii\web\Request::getQueryParams() #9

Closed maximal closed 2 years ago

maximal commented 2 years ago

I have 'enablePrettyUrl' => true, 'showScriptName' => false, and 'news/<type:\w+>' => 'news/index' rule in my urlManager config. While using FPM approach, I can get type param (when accessing /news/some-type URL) by calling Yii::$app->request->get('type'). When using yii\Psr7\web\Request and your bridge, I get nulls on every get() or post() call.

Could you please describe how to get it working?

charlesportwoodii commented 2 years ago

Hi @maximal,

Take a look at the tests directory, and verify you're using a compatible PSR-7/15 Dispatcher. php-fpm isn't a PSR dispatcher so you'll need to use something like RoadRunner, Diactoros, A working example is there:

config: https://github.com/charlesportwoodii/yii2-psr7-bridge/blob/master/tests/config/config.php

return [
    // Other flags
    'components' => [
        'request' => [
            'class' => \yii\Psr7\web\Request::class,
        ],
        'response' => [
            'class' => \yii\Psr7\web\Response::class
        ],
        // Other components
    ]
];

https://github.com/charlesportwoodii/yii2-psr7-bridge/blob/master/tests/ApplicationTest.php#L80

maximal commented 2 years ago

@charlesportwoodii, thanks for the reply!

I’m running the app through RoadRunner (FPM variant is just for comparison). My web.php config is:

'components' => [
    'request' => [
        'cookieValidationKey' => 'secret_key',
        'csrfParam' => '_myCsrf',
        // PSR7 compatible class for RoadRunner support
        'class' => \yii\Psr7\web\Request::class,
    ],
    'response' => [
        // PSR7 compatible class for RoadRunner support
        'class' => \yii\Psr7\web\Response::class
    ],
    // ... ... ...
    'urlManager' => [
        'enablePrettyUrl' => true,
        'showScriptName' => false,
        'rules' => [
            '' => 'site/index',
            'news/<type:\w+>' => 'admin/news/index',
        ],
    ],
],

var_dump(Yii::$app->request::class) gives \yii\Psr7\web\Request, so I guess class replacement is done right.

Related Composer packages are:

"yiisoft/yii2": "^2.0.14",
"spiral/roadrunner": "dev-master",
"nyholm/psr7": "^1.5",
"charlesportwoodii/yii2-psr7-bridge": "dev-master",
charlesportwoodii commented 2 years ago

I haven't touched this repo in a few years. Last I checked Road Runner 1.8 worked, and there was a MR added for 2.5 that worked as well. Maybe try locking spiral/roadrunner package to one of those instead of their development branch? spiral/roadrunner gets pulled in by this repo already.

charlesportwoodii commented 2 years ago

I just spun up the yii2-app-basic app using road-runner and it worked. Here's my configuration an the resulting screenshot. I pulled all this from either rr's documentation or the docs from this one.

.rr.yaml

---
http:
  address: "0.0.0.0:8081"
  pool.debug: true
  middleware: [ "static" ]
  static:
    dir: "./web"
rpc:
  enable: true
server:
  command: ./web/roadrunner
  env:
    YII_ALIAS_WEB: "http://127.0.0.1:8081"
    YII_ALIAS_WEBROOT: ./web

web/roadrunner

#!/usr/bin/env php
<?php

ini_set('display_errors', 'stderr');

// Set your normal YII_ definitions
defined('YII_DEBUG') or define('YII_DEBUG', true);
// Alternatives set this in your rr.yaml file
//defined('YII_DEBUG') or define('YII_DEBUG', \getenv('YII_DEBUG'));

defined('YII_ENV') or define('YII_ENV', 'dev');
// Alternatives set this in your rr.yaml file
//defined('YII_ENV') or define('YII_ENV', \getenv('YII_ENV'));

require __DIR__ . '/../vendor/autoload.php';
require __DIR__ . '/../vendor/yiisoft/yii2/Yii.php';

$worker = Spiral\RoadRunner\Worker::create();
$psrServerFactory = new Laminas\Diactoros\ServerRequestFactory();
$psrStreamFactory = new Laminas\Diactoros\StreamFactory();
$psrUploadFileFactory = new Laminas\Diactoros\UploadedFileFactory();
$psr7 = new Spiral\RoadRunner\Http\PSR7Worker($worker, $psrServerFactory, $psrStreamFactory, $psrUploadFileFactory);

$config = require __DIR__ . '/../config/web.php';

$application = (new \yii\Psr7\web\Application($config));

// Handle each request in a loop
try {
    while ($request = $psr7->waitRequest()) {
        if (($request instanceof Psr\Http\Message\ServerRequestInterface)) {
            try {
                $response = $application->handle($request);
                $psr7->respond($response);
            } catch (\Throwable $e) {
                $psr7->getWorker()->error((string)$e);
            }

            if ($application->clean()) {
                $psr7->getWorker()->stop();
                return;
            }
        }
    }
} catch (\Throwable $e) {
    $psr7->getWorker()->error((string)$e);
}

composer.json

"require": {
        "php": ">=5.6.0",
        "yidas/yii2-bower-asset": "2.0.13.1",
        "yiisoft/yii2": "~2.0.14",
        "yiisoft/yii2-bootstrap4": "~2.0.0",
        "yiisoft/yii2-swiftmailer": "~2.0.0 || ~2.1.0",
        "charlesportwoodii/yii2-psr7-bridge": "dev-master",
        "spiral/roadrunner": "^2.9"
    }

web/config.php

[
 'aliases' => [
        '@bower' => '@vendor/yidas/yii2-bower-asset/bower',
        '@npm'   => '@vendor/npm-asset',
    ],
'request' => [
            'class' => \yii\Psr7\web\Request::class,
            'enableCookieValidation' => false,
            'enableCsrfValidation' => false,
            'enableCsrfCookie' => false,
            'cookieValidationKey' => 'foo',
            'parsers' => [
                'application/json' => \yii\web\JsonParser::class,
            ]
        ],
        'response' => [
            'class' => \yii\Psr7\web\Response::class,
            'charset' => 'UTF-8'
        ],
]

This is also the latest spiral/roadrunner.

Double check your configuration. Start with the basic app, then port things over to your custom app.

Screen Shot 2022-04-13 at 11 24 15

maximal commented 2 years ago

@charlesportwoodii, I cannot see Laminas\Diactoros dependency in your composer.json. Which version do you use? I’m using a different implementation (nyholm/psr7), maybe the problem is here.

charlesportwoodii commented 2 years ago

It's pulled in: https://github.com/charlesportwoodii/yii2-psr7-bridge/blob/master/composer.json#L17.

The example implementation uses it. You don't have to use Diactoros you can use any PSR dispatcher you want - the example provided is just as an example implementation.

maximal commented 2 years ago

Tried both (nyholm/psr7 and laminas/laminas-diactoros) PSR-7 implementations. Couldn’t get standard Yii2 paging widgets working, they’re using ->get() actively.

If we have parameters in URL and simultaneously using paging, then URLs in paging links are broken. /news/index?dp-39-page=2 or something instead of /news/type_name?page=2.

This bridge’s Yii::$app->request->get('type') for parameters in URLs returns null. Standard Yii2 Request implementation returns type_name.

charlesportwoodii commented 2 years ago

You're using the runner and configuration I provided, correct? The example Dispatcher/rr worker is very tied to laminas/laminas-diactoros so if you aren't using that exact Dispatcher and folder structure you'll need to make your own. The examples are provided for illustrative purposes only, but they do work -

I can also verify that the request object works just fine with both pretty URLs. See the following gif and code blocks. As you can see the URL parameters change and this works just fine with enablePrettyUrls set and the script name hidden.

Kapture 2022-04-13 at 14 18 13

// SiteController.php
public function actionPager()
    {
        $provider = new \yii\data\ArrayDataProvider([
            'allModels' => [
                [ 1, "test1", "test1@example.com"],
                [ 2, "test2", "test2@example.com"],
                [ 3, "test3", "test3@example.com"],
                [ 4, "test4", "test4@example.com"],
                [ 5, "test5", "test5@example.com"],
                [ 6, "test6", "test6@example.com"],
                [ 7, "test7", "test7@example.com"],
                [ 8, "test8", "test8@example.com"],
                [ 9, "test9", "test9@example.com"],
            ],
            'sort' => [
                'attributes' => ['id', 'username', 'email'],
            ],
            'pagination' => [
                'pageSize' => 10,
            ],
        ]);

        $provider->pagination->pageSize=2;

        return $this->render('pager', [
            'dataProvider' => $provider
        ]);
    }
// views/pager.php
<?php

/** @var yii\web\View $this */

$this->title = 'My Yii Application';
echo \yii\grid\GridView::widget([
    'dataProvider' => $dataProvider,
]);

echo \yii\widgets\LinkPager::widget([
    'pagination' => $dataProvider->pagination,
]);

All yii/psr7/web/Request does is translate the PSR7 request object into the appropriate Yii2 models, so if it's not doing that, then something is wrong with your runner/dispatcher. Have you tried the code I provided?

A few things I recommend you try:

  1. Use the exact .rr.yml, composer.json, and web/roadrunner worker I provided in the previous comment and verify your config is setup correctly.
  2. If you're using a different dispatcher, verify you're pushing the PSR7 object into the psr7/web/Application instance exported by this package.
  3. Try running this on the yii-web-basic application to see how it behaves and it get working there.
  4. Check the raw PSR7 object in Yii2 (Yii::$app->request->getPsr7Request()) and verify that the PSR7 object has your request parameters. If it doesn't then something isn't quite right with your dispatcher.

Try these first, especially just testing this in the yii-web-basic app and verify it meets your needs before trying it in your other app. This is alpha software but it does work I use it rather extensively in several private projects.

Regarding:

This bridge’s Yii::$app->request->get('type') for parameters in URLs returns null. Standard Yii2 Request implementation returns type_name.

I don't think yii\web\Request::get() has ever returned named parameters, I believe the standard way to achieve this is to pass them as action arguments, like so

public function actionIndex($type) {}

maps to

'news/<type:\s+>' => 'admin/news/index'

yii\web\Request::get() is only documented to return $_GET parameters.

Screen Shot 2022-04-13 at 14 27 25

I recommend reviewing the Yii2 documentation, and making sure you've actually go thing setup correctly. Sounds like there's other issues you need to address first before getting into this.

maximal commented 2 years ago

Seems like the problem is in yii\widgets\ListView. For some reason I don’t understand it doesn’t initiate its ->pager->pagination properties with the params I passed.

My view:

<?= ListView::widget([
    'options' => ['tag' => null],
    'itemOptions' => ['tag' => null],
    'layout' => "<div>{items}</div>\n<div>{pager}</div>",
    'dataProvider' => $dataProvider,
    'itemView' => 'item',
    'pager' => [
        'pagination' => [
            'params' => ['alias' => $model->url, '#' => 'asdf'],

            // Should fail due to setting unknown property, but it won’t
            'any_string' => 'dummy',
        ],
        // Fails as expected if uncommented
        //'any_string' => 'dummy',
    ],
]) ?>

Pagination::createUrl() method:

public function createUrl($page, $pageSize = null, $absolute = false)
{
    $page = (int) $page;
    $pageSize = (int) $pageSize;

    // 
    // Here it doesn’t see my URL params passed to `widget()` initiation method.
    // It doesn’t see them in both cases (`yii\web\Request` and `yii\Psr7\web\Request`),
    // but `yii\web\Request::getQueryParams()` gets them, so the app works fine in this case
    // and won’t work in `yii\Psr7\web\Request` case.
    //

    if (($params = $this->params) === null) {
        $request = Yii::$app->getRequest();
        $params = $request instanceof Request ? $request->getQueryParams() : [];
    }
    if ($page > 0 || $page == 0 && $this->forcePageParam) {
        $params[$this->pageParam] = $page + 1;
    } else {
        unset($params[$this->pageParam]);
    }
    if ($pageSize <= 0) {
        $pageSize = $this->getPageSize();
    }
    if ($pageSize != $this->defaultPageSize) {
        $params[$this->pageSizeParam] = $pageSize;
    } else {
        unset($params[$this->pageSizeParam]);
    }
    $params[0] = $this->route === null ? Yii::$app->controller->getRoute() : $this->route;
    $urlManager = $this->urlManager === null ? Yii::$app->getUrlManager() : $this->urlManager;
    if ($absolute) {
        return $urlManager->createAbsoluteUrl($params);
    }

    return $urlManager->createUrl($params);
}

Investigating further.

maximal commented 2 years ago

Maybe @samdark could take a look?

charlesportwoodii commented 2 years ago

I am not a member ofthe Yii2 team, and samdark is not affiliated with this project in any way.

Please re-view the instructions I previously provided, your runner configuration is not setup correctly. yii\widgets\ListView will work once you configure your runner as outlined here: https://github.com/charlesportwoodii/yii2-psr7-bridge/issues/9#issuecomment-1098250099

I can confirm that yii\widgets\ListView works with that configuration. If you are using a custom configuration, dispatcher, or anything beyond what I have provided I'm afraid I'm not really able to provide any support as the examples I have provided work.

Kapture 2022-04-21 at 10 03 00

The example listed here is using the exact same configuration as I've provided above, and the relevant controller, and view files are listed as follows.

//controllers/SiteController.php


    public function actionPager()
    {
        $provider = new \yii\data\ArrayDataProvider([
            'allModels' => [
                [ 1, "test1", "test1@example.com"],
                [ 2, "test2", "test2@example.com"],
                [ 3, "test3", "test3@example.com"],
                [ 4, "test4", "test4@example.com"],
                [ 5, "test5", "test5@example.com"],
                [ 6, "test6", "test6@example.com"],
                [ 7, "test7", "test7@example.com"],
                [ 8, "test8", "test8@example.com"],
                [ 9, "test9", "test9@example.com"],
            ],
            'sort' => [
                'attributes' => ['id', 'username', 'email'],
            ],
            'pagination' => [
                'pageSize' => 1,
                'defaultPageSize' => 1
            ],
        ]);

        return $this->render('pager', [
            'dataProvider' => $provider
        ]);
    }

//views/pager.php

<h2>This is a \yii\widgets\ListView instance</h2>

<?php

echo \yii\widgets\ListView::widget([
    'layout' => "<div>{items}</div><br /><br /><div>{pager}</div>",
    'dataProvider' => $dataProvider,
    'itemView' => 'item'
]);

//views/item.php

<h4>This is an item view item.php for <?php echo $model[1]; ?></h4>

<?php var_dump($model);

I must re-iterate that you please try the examples I have provided, exactly as they are provided, before adapting things to ensure it meets you requirements. As I have verified this works with yii\widgets\ListView (and the test suite that is attached verifies it work as well), I will be closing and locking this issue for further discussion. If you have specific issues after testing with the yii2-app-basic and implementing the runner exactly as I have outlined in the linked comment I'll be happy to review them, however as it stands I am neither able to reproduce this or verify this as an issue.

charlesportwoodii commented 2 years ago

@maximal Please review #10 and see if it resolves your issue and thanks for your patience. I didn't understand the issue you were reporting as the URL Parameters != Query Parameters but Yii2 does some (imo bad) weird mapping resulting in them being returned with yii\web\Request::getQueryParameters().

I've updated the issue title to make that more clear, but please review the MR to see if it covers your needs.

maximal commented 2 years ago

Sorry for being not accurate in my description of this issue.

With #10 it works fine, thank you!