Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 19 additions & 3 deletions src/Parser/Parser.php
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ public function parse(string $code, string $templatePath = 'eval code'): array

$state = $parserStatus->getState();
$scannerStatus = ScannerStatus::OK;
$prevToken = 0;

while (($scannerStatus = $scanner->scanForToken()) === ScannerStatus::OK) {
$token = $scanner->getToken();
Expand Down Expand Up @@ -131,7 +132,8 @@ public function parse(string $code, string $templatePath = 'eval code'): array
$parser,
$parserStatus,
$token,
$state
$state,
$prevToken
),
CompilerOpcode::ENDSWITCH->value => $this->handleEndswitch($parser, $parserStatus, $state),
CompilerOpcode::RAW_FRAGMENT->value => $this->handleRawFragment(
Expand Down Expand Up @@ -181,6 +183,11 @@ public function parse(string $code, string $templatePath = 'eval code'): array
break;
}

// whitespace inside delimiters arrives as IGNORE; skip it
if ($opcode !== CompilerOpcode::IGNORE->value) {
$prevToken = $opcode;
}

$state->setEnd($state->getStart());
}

Expand Down Expand Up @@ -285,13 +292,22 @@ private function handleCase(phvolt_Parser $parser, Status $parserStatus): void
$parser->phvolt_(Opcode::CASE->value);
}

/**
* "default" is the {% default %} clause only when it is inside a switch
* and directly follows the opening delimiter; anywhere else (e.g. the
* |default() filter) it is a plain identifier.
*/
private function handleDefault(
phvolt_Parser $parser,
Status $parserStatus,
Token $token,
State $state
State $state,
int $prevToken
): void {
if ($state->getSwitchLevel() !== 0) {
if (
$state->getSwitchLevel() !== 0 &&
$prevToken === CompilerOpcode::OPEN_DELIMITER->value
) {
$parser->phvolt_(Opcode::DEFAULT->value);

return;
Expand Down
165 changes: 165 additions & 0 deletions tests/unit/Compiler/CompileSwitchTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
<?php

/**
* This file is part of the Phalcon Framework.
*
* (c) Phalcon Team <[email protected]>
*
* For the full copyright and license information, please view the LICENSE.txt
* file that was distributed with this source code.
*/

declare(strict_types=1);

namespace Phalcon\Tests\Unit\Compiler;

use Phalcon\Volt\Compiler;
use PHPUnit\Framework\TestCase;

final class CompileSwitchTest extends TestCase
{
private Compiler $compiler;

public function setUp(): void
{
$this->compiler = new Compiler();
}

/**
* Tests the "default" filter inside a case block of a switch
*
* @return void
*
* @author Phalcon Team <[email protected]>
* @since 2026-06-10
*
* @issue https://github.com/phalcon/cphalcon/issues/16003
*/
public function testMvcViewEngineVoltCompilerCompileSwitchCaseWithDefaultFilter(): void
{
$actual = $this->compiler->compileString(
"{% set aNumber = 1 %}
{% switch aNumber %}
{% case 0 %}
{{ greatText }}
{% break %}
{% case 1 %}
{{ false|default('simple text') }}
{% break %}
{% endswitch %}"
);

$this->assertStringContainsString('switch ($aNumber):', $actual);
$this->assertStringContainsString('case 0:', $actual);
$this->assertStringContainsString('case 1:', $actual);
$this->assertStringContainsString(
"(empty(false) ? ('simple text') : (false))",
$actual
);
}

/**
* Tests the {% default %} clause surrounded by extra whitespace
*
* @return void
*
* @author Phalcon Team <[email protected]>
* @since 2026-06-10
*
* @issue https://github.com/phalcon/cphalcon/issues/16003
*/
public function testMvcViewEngineVoltCompilerCompileSwitchDefaultClauseWhitespace(): void
{
$actual = $this->compiler->compileString(
"{% switch x %}{% case 1 %}one{% default %}other{% endswitch %}"
);

$this->assertStringContainsString('default:', $actual);
}

/**
* Tests the {% default %} clause with whitespace control markers
*
* @return void
*
* @author Phalcon Team <[email protected]>
* @since 2026-06-10
*
* @issue https://github.com/phalcon/cphalcon/issues/16003
*/
public function testMvcViewEngineVoltCompilerCompileSwitchDefaultClauseWhitespaceControl(): void
{
$actual = $this->compiler->compileString(
"{% switch x %}{% case 1 %}one{%- default -%}other{% endswitch %}"
);

$this->assertStringContainsString('default:', $actual);
}

/**
* Tests the "default" filter inside the {% default %} clause itself
*
* @return void
*
* @author Phalcon Team <[email protected]>
* @since 2026-06-10
*
* @issue https://github.com/phalcon/cphalcon/issues/16003
*/
public function testMvcViewEngineVoltCompilerCompileSwitchDefaultClauseWithDefaultFilter(): void
{
$actual = $this->compiler->compileString(
"{% switch x %}{% case 1 %}one{% default %}"
. "{{ value|default('unknown') }}{% endswitch %}"
);

$this->assertStringContainsString('default:', $actual);
$this->assertStringContainsString(
"(empty(\$value) ? ('unknown') : (\$value))",
$actual
);
}

/**
* Tests the "default" filter outside a switch (issue #13242 regression)
*
* @return void
*
* @author Phalcon Team <[email protected]>
* @since 2026-06-10
*
* @issue https://github.com/phalcon/cphalcon/issues/16003
*/
public function testMvcViewEngineVoltCompilerCompileSwitchDefaultFilterOutsideSwitch(): void
{
$actual = $this->compiler->compileString(
"{{ value|default('unknown') }}"
);

$this->assertStringContainsString(
"(empty(\$value) ? ('unknown') : (\$value))",
$actual
);
}

/**
* Tests "default" used as a plain identifier inside a switch
*
* @return void
*
* @author Phalcon Team <[email protected]>
* @since 2026-06-10
*
* @issue https://github.com/phalcon/cphalcon/issues/16003
*/
public function testMvcViewEngineVoltCompilerCompileSwitchDefaultIdentifierInsideSwitch(): void
{
$actual = $this->compiler->compileString(
"{% switch x %}{% case 1 %}"
. "{% set default = 'abc' %}{{ default }}{% endswitch %}"
);

$this->assertStringContainsString("\$default = 'abc'", $actual);
$this->assertStringContainsString('<?= $default ?>', $actual);
}
}
164 changes: 164 additions & 0 deletions tests/unit/Compiler/SwitchTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,170 @@ public function testMvcViewEngineVoltParserSwitchCase(): void
$this->assertSame($expected, $actual);
}

/**
* Tests the "default" filter inside a case block of a switch
*
* @return void
*
* @author Phalcon Team <[email protected]>
* @since 2026-06-10
*
* @issue https://github.com/phalcon/cphalcon/issues/16003
*/
public function testMvcViewEngineVoltParserSwitchCaseDefaultFilter(): void
{
$source = "{% switch x %}{% case 1 %}"
. "{{ false|default('simple text') }}{% break %}{% endswitch %}";
$expected = [
[
'type' => 411,
'expr' => [
'type' => 265,
'value' => 'x',
'file' => 'eval code',
'line' => 1,
],
'case_clauses' => [
[
'type' => 412,
'expr' => [
'type' => 258,
'value' => '1',
'file' => 'eval code',
'line' => 1,
],
'file' => 'eval code',
'line' => 1,
],
[
'type' => 359,
'expr' => [
'type' => 124,
'left' => [
'type' => 262,
'file' => 'eval code',
'line' => 1,
],
'right' => [
'type' => 350,
'name' => [
'type' => 265,
'value' => 'default',
'file' => 'eval code',
'line' => 1,
],
'arguments' => [
[
'expr' => [
'type' => 260,
'value' => 'simple text',
'file' => 'eval code',
'line' => 1,
],
'file' => 'eval code',
'line' => 1,
],
],
'file' => 'eval code',
'line' => 1,
],
'file' => 'eval code',
'line' => 1,
],
'file' => 'eval code',
'line' => 1,
],
[
'type' => 320,
'file' => 'eval code',
'line' => 1,
],
],
'file' => 'eval code',
'line' => 1,
],
];
$actual = $this->compiler->parse($source);
$this->assertSame($expected, $actual);
}

/**
* Tests the "default" filter inside the {% default %} clause itself
*
* @return void
*
* @author Phalcon Team <[email protected]>
* @since 2026-06-10
*
* @issue https://github.com/phalcon/cphalcon/issues/16003
*/
public function testMvcViewEngineVoltParserSwitchDefaultClauseDefaultFilter(): void
{
$source = "{% switch x %}{% default %}"
. "{{ value|default('unknown') }}{% endswitch %}";
$expected = [
[
'type' => 411,
'expr' => [
'type' => 265,
'value' => 'x',
'file' => 'eval code',
'line' => 1,
],
'case_clauses' => [
[
'type' => 413,
'file' => 'eval code',
'line' => 1,
],
[
'type' => 359,
'expr' => [
'type' => 124,
'left' => [
'type' => 265,
'value' => 'value',
'file' => 'eval code',
'line' => 1,
],
'right' => [
'type' => 350,
'name' => [
'type' => 265,
'value' => 'default',
'file' => 'eval code',
'line' => 1,
],
'arguments' => [
[
'expr' => [
'type' => 260,
'value' => 'unknown',
'file' => 'eval code',
'line' => 1,
],
'file' => 'eval code',
'line' => 1,
],
],
'file' => 'eval code',
'line' => 1,
],
'file' => 'eval code',
'line' => 1,
],
'file' => 'eval code',
'line' => 1,
],
],
'file' => 'eval code',
'line' => 1,
],
];
$actual = $this->compiler->parse($source);
$this->assertSame($expected, $actual);
}

/**
* @return void
*
Expand Down
Loading