Skip to content

Commit 0029d24

Browse files
committed
[8.x] Introduce JsString for encoding data for use in JavaScript
1 parent f429789 commit 0029d24

File tree

4 files changed

+270
-46
lines changed

4 files changed

+270
-46
lines changed

src/Illuminate/Support/Js.php

Lines changed: 0 additions & 29 deletions
This file was deleted.

src/Illuminate/Support/JsString.php

Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
<?php
2+
3+
namespace Illuminate\Support;
4+
5+
use Illuminate\Contracts\Support\Arrayable;
6+
use Illuminate\Contracts\Support\Htmlable;
7+
use Illuminate\Contracts\Support\Jsonable;
8+
use JsonSerializable;
9+
10+
class JsString implements Htmlable
11+
{
12+
13+
/**
14+
* Flags that must always be used when encoding to JSON for JsString.
15+
*
16+
* @var int
17+
*/
18+
protected const REQUIRED_FLAGS = JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_AMP | JSON_HEX_QUOT | JSON_THROW_ON_ERROR;
19+
20+
/**
21+
* The javascript string.
22+
*
23+
* @var string
24+
*/
25+
protected $js;
26+
27+
/**
28+
* Create a new JsString from data.
29+
*
30+
* @param mixed $data
31+
* @param int $flags
32+
* @param int $depth
33+
*
34+
* @return static
35+
*
36+
* @throws \JsonException
37+
*/
38+
public static function from($data, $flags = 0, $depth = 512)
39+
{
40+
return new static($data, $flags, $depth);
41+
}
42+
43+
/**
44+
* Create a new JsString.
45+
*
46+
* @param mixed $data
47+
* @param int|null $flags
48+
* @param int $depth
49+
*
50+
* @return void
51+
*
52+
* @throws \JsonException
53+
*/
54+
public function __construct($data, $flags = 0, $depth = 512)
55+
{
56+
$this->js = $this->convertDataToJavaScriptExpression($data, $flags, $depth);
57+
}
58+
59+
/**
60+
* Get string representation of data for use in HTML.
61+
*
62+
* @return string
63+
*/
64+
public function toHtml()
65+
{
66+
return $this->js;
67+
}
68+
69+
/**
70+
* Get string representation of data for use in HTML.
71+
*
72+
* @return string
73+
*/
74+
public function __toString()
75+
{
76+
return $this->toHtml();
77+
}
78+
79+
/**
80+
* Convert data to a JavaScript expression.
81+
*
82+
* @param mixed $data
83+
* @param int $flags
84+
* @param int $depth
85+
*
86+
* @return string
87+
*
88+
* @throws \JsonException
89+
*/
90+
protected function convertDataToJavaScriptExpression($data, $flags = 0, $depth = 512)
91+
{
92+
$json = $this->jsonEncode($data, $flags, $depth);
93+
94+
if (is_string($data)) {
95+
return "'".substr($json, 1, '-1')."'";
96+
}
97+
98+
return $this->convertJsonToJavaScriptExpression($json, $flags);
99+
}
100+
101+
/**
102+
* Encode data as JSON.
103+
*
104+
* @param mixed $data
105+
* @param int $flags
106+
* @param int $depth
107+
*
108+
* @return string
109+
*
110+
* @throws \JsonException
111+
*/
112+
protected function jsonEncode($data, $flags = 0, $depth = 512)
113+
{
114+
if ($data instanceof Jsonable) {
115+
return $data->toJson($flags | static::REQUIRED_FLAGS);
116+
}
117+
118+
if ($data instanceof Arrayable && ! ($data instanceof JsonSerializable)) {
119+
$data = $data->toArray();
120+
}
121+
122+
return json_encode($data, $flags | static::REQUIRED_FLAGS, $depth);
123+
}
124+
125+
/**
126+
* Convert JSON to a JavaScript expression.
127+
*
128+
* @param string $json
129+
* @param int $flags
130+
*
131+
* @return string
132+
*
133+
* @throws \JsonException
134+
*/
135+
protected function convertJsonToJavaScriptExpression($json, $flags = 0)
136+
{
137+
if ('[]' === $json || '{}' === $json) {
138+
return $json;
139+
}
140+
141+
if (Str::startsWith($json, ['"', '{', '['])) {
142+
$json = json_encode($json, $flags | static::REQUIRED_FLAGS);
143+
144+
return "JSON.parse('".substr($json, 1, -1)."')";
145+
}
146+
147+
return $json;
148+
}
149+
}

tests/Support/SupportJsStringTest.php

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
<?php
2+
3+
namespace Illuminate\Tests\Support;
4+
5+
use Illuminate\Contracts\Support\Arrayable;
6+
use Illuminate\Contracts\Support\Jsonable;
7+
use Illuminate\Support\JsString;
8+
use JsonSerializable;
9+
use PHPUnit\Framework\TestCase;
10+
11+
class SupportJsStringTest extends TestCase
12+
{
13+
public function testScalars()
14+
{
15+
$this->assertEquals('false', (string) JsString::from(false));
16+
$this->assertEquals('true', (string) JsString::from(true));
17+
$this->assertEquals('1', (string) JsString::from(1));
18+
$this->assertEquals('1.1', (string) JsString::from(1.1));
19+
$this->assertEquals(
20+
"'\\u003Cdiv class=\\u0022foo\\u0022\\u003E\\u0027quoted html\\u0027\\u003C\\/div\\u003E'",
21+
(string) JsString::from('<div class="foo">\'quoted html\'</div>')
22+
);
23+
}
24+
25+
public function testArrays()
26+
{
27+
$this->assertEquals(
28+
"JSON.parse('[\\u0022hello\\u0022,\\u0022world\\u0022]')",
29+
(string) JsString::from(['hello', 'world'])
30+
);
31+
32+
$this->assertEquals(
33+
"JSON.parse('{\\u0022foo\\u0022:\\u0022hello\\u0022,\\u0022bar\\u0022:\\u0022world\\u0022}')",
34+
(string) JsString::from(['foo' => 'hello', 'bar' => 'world'])
35+
);
36+
}
37+
38+
public function testObjects()
39+
{
40+
$this->assertEquals(
41+
"JSON.parse('{\\u0022foo\\u0022:\\u0022hello\\u0022,\\u0022bar\\u0022:\\u0022world\\u0022}')",
42+
(string) JsString::from((object) ['foo' => 'hello', 'bar' => 'world'])
43+
);
44+
}
45+
46+
public function testJsonSerializable()
47+
{
48+
// JsonSerializable should take precedence over Arrayable, so we'll
49+
// implement both and make sure the correct data is used.
50+
$data = new class() implements JsonSerializable, Arrayable {
51+
public $foo = 'not hello';
52+
53+
public $bar = 'not world';
54+
55+
public function jsonSerialize()
56+
{
57+
return ['foo' => 'hello', 'bar' => 'world'];
58+
}
59+
60+
public function toArray()
61+
{
62+
return ['foo' => 'not hello', 'bar' => 'not world'];
63+
}
64+
};
65+
66+
$this->assertEquals(
67+
"JSON.parse('{\\u0022foo\\u0022:\\u0022hello\\u0022,\\u0022bar\\u0022:\\u0022world\\u0022}')",
68+
(string) JsString::from($data)
69+
);
70+
}
71+
72+
public function testJsonable()
73+
{
74+
// Jsonable should take precedence over JsonSerializable and Arrayable, so we'll
75+
// implement all three and make sure the correct data is used.
76+
$data = new class() implements Jsonable, JsonSerializable, Arrayable {
77+
public $foo = 'not hello';
78+
79+
public $bar = 'not world';
80+
81+
public function toJson($options = 0)
82+
{
83+
return json_encode(['foo' => 'hello', 'bar' => 'world'], $options);
84+
}
85+
86+
public function jsonSerialize()
87+
{
88+
return ['foo' => 'not hello', 'bar' => 'not world'];
89+
}
90+
91+
public function toArray()
92+
{
93+
return ['foo' => 'not hello', 'bar' => 'not world'];
94+
}
95+
};
96+
97+
$this->assertEquals(
98+
"JSON.parse('{\\u0022foo\\u0022:\\u0022hello\\u0022,\\u0022bar\\u0022:\\u0022world\\u0022}')",
99+
(string) JsString::from($data)
100+
);
101+
}
102+
103+
public function testArrayable()
104+
{
105+
$data = new class() implements Arrayable {
106+
public $foo = 'not hello';
107+
108+
public $bar = 'not world';
109+
110+
public function toArray()
111+
{
112+
return ['foo' => 'hello', 'bar' => 'world'];
113+
}
114+
};
115+
116+
$this->assertEquals(
117+
"JSON.parse('{\\u0022foo\\u0022:\\u0022hello\\u0022,\\u0022bar\\u0022:\\u0022world\\u0022}')",
118+
(string) JsString::from($data)
119+
);
120+
}
121+
}

tests/Support/SupportJsTest.php

Lines changed: 0 additions & 17 deletions
This file was deleted.

0 commit comments

Comments
 (0)