Repository: shufo/blade-formatter (ts)

Issue:
[Formatting Bug]: @php block formatting should more closely match output of plugin-php
### Version

1.40.1

### Template before formatting

```blade
@php
    $a =
        'arstasrtarstasrtarstasrtarstasrtarstasrtarstasrtarstasrtarstasrtarstasrss';

    $arstasrtarstasrtarstasrtarstasrtarstasrtarstasrtarstasrtarstasrtarstasr = [
        'arstasrtarstasrtarstasrtarstasrtarstasrtarstasrtarstasrtarstasrtarstasr',
        [
            'background-color: red',
            'font-weight: bold' => $isActive,
            'font-weight: bold' => $isActive,
        ],
    ];

    $categories = App\Models\Category::whereIn('id', $catids)
        ->orderBy('description')
        ->get();

    $zipIsForShipping = !(
        $order->isGroupPart() ||
        $order->isPickup() ||
        $order->isDepot() ||
        $order->isNofaDepot()
    );

    $data = [
        [
            'href' => '#',
            'title' => 'Hi!',
            'title' => 'Hi!',
            'title' => 'Hi!',
            'title' => 'Hi!',
        ],
    ];
@endphp
```


### Template after formatting

```blade
@php
    $a = 'arstasrtarstasrtarstasrtarstasrtarstasrtarstasrtarstasrtarstasrtarstasrss';

    $arstasrtarstasrtarstasrtarstasrtarstasrtarstasrtarstasrtarstasrtarstasr = ['arstasrtarstasrtarstasrtarstasrtarstasrtarstasrtarstasrtarstasrtarstasr', ['background-color: red', 'font-weight: bold' => $isActive, 'font-weight: bold' => $isActive]];

    $categories = App\Models\Category::whereIn('id', $catids)->orderBy('description')->get();

    $zipIsForShipping = !($order->isGroupPart() || $order->isPickup() || $order->isDepot() || $order->isNofaDepot());

    $data = [
        [
            'href' => '#',
            'title' => 'Hi!',
            'title' => 'Hi!',
            'title' => 'Hi!',
            'title' => 'Hi!',
        ],
    ];
@endphp
```


### Expected Behaviour

The code in the input template has been formatted w/ `@prettier/plugin-php` and copied into the blade template, but blade-formatter then crams nearly every statement onto a single line. I would expect it to format the code more or less unchanged to match how the PHP plugin formats things.

```php
@php
    $a =
        'arstasrtarstasrtarstasrtarstasrtarstasrtarstasrtarstasrtarstasrtarstasrss';

    $arstasrtarstasrtarstasrtarstasrtarstasrtarstasrtarstasrtarstasrtarstasr = [
        'arstasrtarstasrtarstasrtarstasrtarstasrtarstasrtarstasrtarstasrtarstasr',
        [
            'background-color: red',
            'font-weight: bold' => $isActive,
            'font-weight: bold' => $isActive,
        ],
    ];

    $categories = App\Models\Category::whereIn('id', $catids)
        ->orderBy('description')
        ->get();

    $zipIsForShipping = !(
        $order->isGroupPart() ||
        $order->isPickup() ||
        $order->isDepot() ||
        $order->isNofaDepot()
    );

    $data = [
        [
            'href' => '#',
            'title' => 'Hi!',
            'title' => 'Hi!',
            'title' => 'Hi!',
            'title' => 'Hi!',
        ],
    ];
@endphp
```

Note that part of this template is copied from https://github.com/shufo/prettier-plugin-blade/issues/266. If I format this template w/ blade-formatter 1.38.6, most of the template after formatting is the same, but the chained method call (3rd statement) is formatted correctly.

### Relevant log output

_No response_

Pull request description:
feat: improve formatting of @php blocks (close #908)
## Description
This updates the formatting logic so that "multiline, multi-statement" blocks (eg `@php`) blocks are formatted while respecting the configured `printWidth` or `wrap-line-length` parameter. I also try to dynamically adjust the printWidth based on the current indent level, so that more-deeply indented lines are formatted onto shorter and shorter lines ... just as the `@prettier/plugin-php` would do for more deeply nested lines.

## Related Issue
This should close #908 and, I think, should also allow https://github.com/shufo/prettier-plugin-blade/issues/266 to be closed.

## Motivation and Context
I mostly consume this project via the prettier plugin, and I usually want the formatting of PHP in Blade files to match what it would look like in regular PHP files, which I also format with prettier. The current behavior of "wrapping" nearly everything onto a single line doesn't seem intuitive to me, but – if this behavior is intentional – I could add a config flag to control this.

## How Has This Been Tested?
I have added a few tests for this, and I also ran it against the example template in #908. It produced the expected output.

## Screenshots (if appropriate):
n/a

Thank you!

Patch:
diff --git a/src/formatter.ts b/src/formatter.ts
index 83e27ab..b1b4feb 100644
--- a/src/formatter.ts
+++ b/src/formatter.ts
@@ -1293,11 +1293,28 @@ export default class Formatter {
         const matchedLine = content.match(new RegExp(`^(.*?)${placeholder}`, 'gmi')) ?? [''];
         const indent = detectIndent(matchedLine[0]);
 
-        if (this.isInline(rawBlock) && (await this.isMultilineStatement(rawBlock))) {
+        const isOnSingleLine = this.isInline(rawBlock);
+        const isMultipleStatements = await this.isMultilineStatement(rawBlock);
+        if (isOnSingleLine && isMultipleStatements) {
+          // multiple statements on a single line
           rawBlock = (await util.formatStringAsPhp(`<?php\n${rawBlock}\n?>`, this.options)).trim();
-        } else if (rawBlock.split('\n').length > 1) {
+        } else if (isMultipleStatements) {
+          // multiple statments on mult lines
+
+          const indentLevel = (indent.amount + 1) * this.indentSize;
+
+          rawBlock = (
+            await util.formatStringAsPhp(`<?php${rawBlock}?>`, {
+              ...this.options,
+              useProjectPrintWidth: true,
+              adjustPrintWidthBy: indentLevel,
+            })
+          ).trimEnd();
+        } else if (!isOnSingleLine) {
+          // single statement on mult lines
           rawBlock = (await util.formatStringAsPhp(`<?php${rawBlock}?>`, this.options)).trimEnd();
         } else {
+          // single statement on single line
           rawBlock = `<?php${rawBlock}?>`;
         }
 
diff --git a/src/util.ts b/src/util.ts
index fe3b5f0..f5e66e1 100644
--- a/src/util.ts
+++ b/src/util.ts
@@ -65,10 +65,12 @@ export async function formatStringAsPhp(content: any, params: FormatPhpOption =
     ...params,
   };
 
+  const adjust = params.adjustPrintWidthBy ?? 0;
+  const printWidth = params.useProjectPrintWidth ? options.printWidth - adjust : printWidthForInline;
   try {
     return await prettier.format(content.replace(/\n$/, ''), {
       parser: 'php',
-      printWidth: 1000,
+      printWidth,
       singleQuote: !options.noSingleQuote,
       // @ts-ignore
       phpVersion: options.phpVersion,


Test patch:
diff --git a/__tests__/formatter.test.ts b/__tests__/formatter.test.ts
index 01f5b7d..7cfbdfe 100644
--- a/__tests__/formatter.test.ts
+++ b/__tests__/formatter.test.ts
@@ -5339,4 +5339,82 @@ describe('formatter', () => {
 
     await util.doubleFormatCheck(content, expected);
   });
+
+  test('@php blocks should wrap long statements (#908)', async () => {
+    const content = [
+      `@php`,
+      `$categories = App\\Models\\Category::whereIn('id', $catids)`,
+      `->orderBy('description')`,
+      `->orderBy('description')`,
+      `->orderBy('description')`,
+      `->get();`,
+      `@endphp`,
+    ].join('\n');
+
+    const expected = [
+      `@php`,
+      `    $categories = App\\Models\\Category::whereIn('id', $catids)`,
+      `        ->orderBy('description')`,
+      `        ->orderBy('description')`,
+      `        ->orderBy('description')`,
+      `        ->get();`,
+      `@endphp`,
+      ``,
+    ].join('\n');
+
+    await util.doubleFormatCheck(content, expected);
+  });
+
+  test('@php blocks should attempt to respect to current indent level', async () => {
+    // This statement is 119 chars long and should fit on 1 line w/ the default print width of 120.
+    // Once it's indented within the @php block, though, the line will exceed 120, so it should have
+    // been indented.
+    let content = [
+      `@php`,
+      `$categories = App\\Models\\Category::whereIn('idss', $catids)`,
+      `->orderBy('description')`,
+      `->orderBy('description')`,
+      `->getNone();`,
+      `@endphp`,
+    ].join('\n');
+
+    let expected = [
+      `@php`,
+      `    $categories = App\\Models\\Category::whereIn('idss', $catids)`,
+      `        ->orderBy('description')`,
+      `        ->orderBy('description')`,
+      `        ->getNone();`,
+      `@endphp`,
+      ``,
+    ].join('\n');
+
+    await util.doubleFormatCheck(content, expected);
+
+    // Now wrap the @php in a <div> and make the PHP statement 4 chars shorter to confirm that it
+    // still works at higher indent levels.
+    content = [
+      `<div>`,
+      `@php`,
+      `$categories = App\\Models\\Category::whereIn('idss', $catids)`,
+      `->orderBy('description')`,
+      `->orderBy('description')`,
+      `->get();`,
+      `@endphp`,
+      `</div>`,
+    ].join('\n');
+
+    expected = [
+      `<div>`,
+      `    @php`,
+      `        $categories = App\\Models\\Category::whereIn('idss', $catids)`,
+      `            ->orderBy('description')`,
+      `            ->orderBy('description')`,
+      `            ->get();`,
+      `    @endphp`,
+      `</div>`,
+      ``,
+    ].join('\n');
+
+    await util.doubleFormatCheck(content, expected);
+  });
 });