Laminas-mvc not using layout, $this->headLink() seems can not append stylesheet

My action, not using a layout:

    public function addAction()
    {
        /** @var ServerForm $form */
        $form = $this->formElementManager->get(ServerForm::class);
        $request = $this->getRequest();

        if (!$request->isPost())
        {
            $vm = new ViewModel(['form' => $form]);
            $vm->setTerminal(true);
            return $vm;
        }

        $post = array_merge_recursive(
            $request->getPost()->toArray(),
            $request->getFiles()->toArray()
        );

        $form->setValidationGroup('servername','isvat','settle','accountsjson','mem','license','seal');
        $form->setData($post);

        if (!$form->isValid())
        {
            $vm = new ViewModel(['form' => $form]);
            $vm->setTerminal(true);
            return $vm;
        }

        $server = new Server();
        $server->exchangeArray($form->getData());

        $result = $this->serverTable->save($server,$this->identity());
        if ($result) return $this->redirect()->toRoute('application/server',['action' => 'list']);

        $vm = new ViewModel(['form' => $form]);
        $vm->setTerminal(true);
        return $vm;
    }

I use <?= $this->headLink() ?> in the view pthml file

<?php
/**
 * @var Laminas\View\Renderer\PhpRenderer $this
 */
$this->form->setAttribute('action', $this->url('application/outcome',['action' => 'add']));
$this->form->prepare();
?>
<!DOCTYPE html>
<html>
<head>
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
    <title>some add page</title>
    <?= $this->headLink() ?>
    <link rel="stylesheet" href="/css/application/global.css">
    <link href="/css/application/outcome/add.css?r=123" rel="stylesheet" type="text/css"/>
    <link href="/js/jquery-ui/jquery-ui-1.12.1/jquery-ui.css" rel="stylesheet" type="text/css"/>
    <script src="/js/jquery-3.5.1.min.js"></script>
    <script src="/js/jquery-validation-1.19.3/jquery.validate.min.js"></script>
    <?= $this->headScript() ?>
</head>
<body>
//....
<?php $this->autocomplete('#servername','/application/server/ajax1') ?>  //a custome view helper here.

in my customed view helper, I append stylesheet to headlink, this step not work.

use Laminas\View\Helper\AbstractHelper;

class AutocompleteHelper extends AbstractHelper
{
    private $isInit = false;
    private static $jslib = 'js/jquery-ui/jquery-ui-1.12.1/jquery-ui.js';
    private static $csslib = 'js/jquery-ui/jquery-ui-1.12.1/jquery-ui.css';

    private $selector;
    private $source;
    private $minLength;

    public function __invoke($selector, $source, $minLength = 1, $type='php')
    {
        $this->selector = $selector;
        if (is_string($source))
        {
            if ($type == 'js')
            {
                $this->source = 'source: function(request, resolve) { resolve('.$source.'); }';
            }
            else
            {
                $this->source = 'source: "'.$source.'"';
            }
        }
        elseif (is_array($source))
        {
            $this->source = 'source: '.json_encode($source);
        }

        $this->minLength = $minLength;

        return $this->render();
    }

    public function render()
    {
        $oRender = $this->getView();
        if (!$this->isInit)
        {
            $oRender->headLink()->appendStylesheet($oRender->basepath(self::$csslib));
            $oRender->inlinescript()->appendFile($oRender->basepath(self::$jslib));
            $this->isInit = true;
        }

        $jsStr = <<<EOF
        $( "{selector}" ).autocomplete({
          {source},
          minLength: {minLength},
        }).on('focus', function() { $(this).keydown(); });
EOF;
        $oRender->inlinescript()->captureStart();
        echo str_replace(['{selector}','{source}','{minLength}'],[$this->selector,$this->source,$this->minLength],$jsStr);
        $oRender->inlinescript()->captureEnd();
    }
}

by the way,

$oRender->headLink()->appendStylesheet($oRender->basepath(self::$csslib)); //not work!!
$oRender->inlinescript()->appendFile($oRender->basepath(self::$jslib)); //do work!!

I found the above phenomenon by looking via the html source code in the web browser.

How to make the headlink appendstylesheet work?

If I understand your code correctly then the HeadLink has nothing to render because you call the your own helper Autocomplete after the HeadLink helper.

AutocompleteHelper and HeadLink Helper should not be placed in one file?

But $oRender->inlinescript()->appendFile($oRender->basepath(self::$jslib)); this work normally.

I found it in the html source code:
1111

Please look at your view script:

<!DOCTYPE html>
<html>
<head>
    …
    <?= $this->headLink() ?>
    …
</head>
<body>
…
<?php $this->autocomplete('#servername','/application/server/ajax1') ?> 

The HeadLink helper is called before the your Autocomplete helper, so there nothing to render.

Here you call the InlineScript helper after your Autocomplete helper.

Ok, I understood! thank you!
The inlinescript helper do at the behind of the autocomplete helper. you are right.

It seems I have to use a layout, since layout rendering the last after the view rendering.

Unfortunately wrong conclusion!

Your own helper is wrong: remove the echo then you can use:

<?php $autocomplete = $this->autocomplete('#servername','/application/server/ajax1') ?> 
<!DOCTYPE html>
<html>
<head>
    …
    <?= $this->headLink() ?>
    …
</head>
<body>
<?= $autocomplete ?>
…
<?= $this->inlineScript() ?>
</body>
</html>

A helper should not give out anything himself, only give something back.

Nice!!

    public function render()
    {
        $oRender = $this->getView();
        if (!$this->isInit)
        {
            $oRender->headLink()->appendStylesheet($oRender->basepath(self::$csslib));
            $oRender->inlinescript()->appendFile($oRender->basepath(self::$jslib));
            $this->isInit = true;
        }

        $jsStr = <<<EOF
        $( "{selector}" ).autocomplete({
          {source},
          minLength: {minLength},
        }).on('focus', function() { $(this).keydown(); });
EOF;
        $oRender->inlinescript()->captureStart();
        echo str_replace(['{selector}','{source}','{minLength}'],[$this->selector,$this->source,$this->minLength],$jsStr);
        $oRender->inlinescript()->captureEnd();
    }

Ah sorry, I did not read the entire helper. The return is wrong here.

You can use:

<?php $this->autocomplete('#servername','/application/server/ajax1') ?> 
<!DOCTYPE html>
<html>
<head>
    …
    <?= $this->headLink() ?>
    …
</head>
<body>
…
<?= $this->inlineScript() ?>
</body>
</html>

As expected, no script wrap around

Outside the html tag?!!
weird…

Your own helper needs the same content like in your first post!

Ok, I see. Thank you!

$oRender->inlinescript()->captureStart();
        echo str_replace(['{selector}','{source}','{minLength}'],[$this->selector,$this->source,$this->minLength],$jsStr);
        $oRender->inlinescript()->captureEnd();

you are so right! this way, the script only appear at the end of the document. It wont be outside of html tag.

I thought it will echo at the place where the helper placed, in fact it only will appear at the inlinescipt().
Perfectly solved the problem, thank you so much!!