Skip to content

Commit 1b3d654

Browse files
committed
feature: apply 1.x
b0f752d 92acc44
1 parent f0189ac commit 1b3d654

7 files changed

Lines changed: 138 additions & 31 deletions

File tree

composer.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,8 @@
1717
"require": {
1818
"php": "^8.1",
1919
"illuminate/container": ">=9",
20-
"sonata-project/exporter": "^3.3"
20+
"sonata-project/exporter": "^3.3",
21+
"symfony/filesystem": ">=5.4"
2122
},
2223
"require-dev": {
2324
"friendsofphp/php-cs-fixer": "^3.51",

src/DataExporter.php

Lines changed: 26 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -40,32 +40,49 @@ public static function __callStatic($name, $arguments)
4040
static::$container = static::getContainer();
4141
}
4242

43-
return new Handler(static::$container, $arguments[0], $name, $arguments[1] ?? []);
43+
return static::$container->make(Handler::class, [
44+
'container' => static::$container,
45+
'source' => $arguments[0],
46+
'writer' => $name,
47+
'writerOptions' => $arguments[1] ?? [],
48+
]);
4449
}
4550

4651
final protected static function getContainer(): ContainerContract
4752
{
4853
$container = static::getContainerInstance();
4954

50-
$container->singleton(Handler::CONTAINER_DATA_EXPORT_CONFIG_KEY, function () {
51-
return array_merge([
52-
'writer' => static::writerConfig(),
53-
'deleteFirstIfExist' => true,
54-
], static::customConfig());
55-
});
55+
if (! $container->has(Handler::CONTAINER_DATA_EXPORT_CONFIG_KEY)) {
56+
$container->singleton(Handler::CONTAINER_DATA_EXPORT_CONFIG_KEY, function () {
57+
return array_merge([
58+
'writer' => static::writerConfig(),
59+
'deleteFirstIfExist' => true,
60+
], static::customConfig());
61+
});
62+
}
5663

5764
return $container;
5865
}
5966

67+
private static $setContainer = null;
68+
69+
public static function setContainer(ContainerContract $container)
70+
{
71+
static::$setContainer = $container;
72+
}
73+
74+
/**
75+
* @return ContainerContract
76+
*/
6077
protected static function getContainerInstance(): ContainerContract
6178
{
62-
return new Container();
79+
return static::$setContainer ?: new Container();
6380
}
6481

6582
/**
6683
* @return array<string, array{class: string, options: array, extension: string}>
6784
*/
68-
protected static function writerConfig(): array
85+
public static function writerConfig(): array
6986
{
7087
return [
7188
'csv' => [

src/DataExporter/Handler.php

Lines changed: 39 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,11 @@
33
namespace Kriss\DataExporter\DataExporter;
44

55
use Illuminate\Contracts\Container\Container as ContainerContract;
6+
use Illuminate\Support\Str;
67
use InvalidArgumentException;
78
use Kriss\DataExporter\Exceptions\FileAlreadyExistException;
89
use Sonata\Exporter\Writer\WriterInterface;
10+
use Symfony\Component\Filesystem\Path;
911
use Symfony\Component\HttpFoundation\ResponseHeaderBag;
1012
use Symfony\Component\HttpFoundation\StreamedResponse;
1113

@@ -79,11 +81,11 @@ public function browserDownload(string $downloadName): StreamedResponse
7981
});
8082

8183
if ($downloadName) {
82-
$name = basename($this->makeFilename($downloadName));
84+
$name = $this->makeFilename($downloadName, false);
8385
$response->headers->set('Content-Disposition', $response->headers->makeDisposition(
8486
ResponseHeaderBag::DISPOSITION_ATTACHMENT,
8587
$name,
86-
'file_' . date('YmdHis') // 使用该名字代替 ascii 控制是因为 Str::ascii 也可能存在内置不支持的情况
88+
str_replace('%', $this->getConfig()['filenameInvalidCharsReplace'], Str::ascii($name))
8789
));
8890
}
8991

@@ -108,27 +110,55 @@ public function clean(): void
108110
/**
109111
* 生成的文件名
110112
* @param string $filename
113+
* @param bool $maybeHasPath filename 中是否可能包含路径
111114
* @return string
112115
*/
113-
public function makeFilename(string $filename): string
116+
public function makeFilename(string $filename, bool $maybeHasPath = true): string
114117
{
115118
if (str_contains($filename, 'php://')) {
116119
return $filename;
117120
}
118-
$extension = $this->getWriterConfig()['extension'];
119-
if (pathinfo($filename, PATHINFO_EXTENSION) !== $extension) {
120-
$filename .= '.' . $extension;
121+
122+
$dirname = '';
123+
$fileBasename = $filename;
124+
if ($maybeHasPath) {
125+
// 格式化文件路径
126+
$filename = Path::canonicalize($filename);
127+
// 截取 / 之后的作为文件名
128+
if (($pos = strrpos($filename, '/')) !== false) {
129+
$dirname = substr($filename, 0, $pos + 1); // 保留 /
130+
$fileBasename = substr($filename, $pos + 1);
131+
}
121132
}
122133

123-
return $filename;
134+
$configExtension = $this->getWriterConfig()['extension'];
135+
if (($pos = strpos($fileBasename, '.' . $configExtension . '?')) !== false) {
136+
// 兼容同扩展名情况下,存在 ?x=1 的情况,比如:abc.csv?x=1,此时直接将 ? 后的删除
137+
$fileBasename = substr($fileBasename, 0, $pos);
138+
}
139+
140+
// 去除特殊符号
141+
$invalidChars = ['/', '\\', ':', '*', '?', '"', '<', '>', '|'];
142+
$fileBasename = str_replace($invalidChars, array_fill(0, count($invalidChars) - 1, $this->getConfig()['filenameInvalidCharsReplace']), $fileBasename); // 替换文件系统不支持的特殊符号
143+
144+
// 补全 ext
145+
if (pathinfo($fileBasename, PATHINFO_EXTENSION) !== $configExtension) {
146+
$fileBasename .= '.' . $configExtension;
147+
}
148+
149+
return $dirname . $fileBasename;
124150
}
125151

126152
/**
127-
* @return array{writer: array, deleteFirstIfExist: bool}
153+
* @return array{writer: array, deleteFirstIfExist: bool, filenameInvalidCharsReplace: string, filenameMaker: ?callable}
128154
*/
129155
private function getConfig(): array
130156
{
131-
return $this->container->get(static::CONTAINER_DATA_EXPORT_CONFIG_KEY);
157+
return array_merge([
158+
'writer' => [],
159+
'deleteFirstIfExist' => true,
160+
'filenameInvalidCharsReplace' => '_',
161+
], $this->container->get(static::CONTAINER_DATA_EXPORT_CONFIG_KEY));
132162
}
133163

134164
/**

tests/Feature/DeleteBeforeWrite.php

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
namespace Kriss\DataExporter\Tests\Feature;
44

5+
use Illuminate\Container\Container;
6+
use Kriss\DataExporter\DataExporter;
57
use Kriss\DataExporter\Exceptions\FileAlreadyExistException;
68

79
class MyDataExport extends \Kriss\DataExporter\DataExporter
@@ -29,3 +31,25 @@ protected static function customConfig(): array
2931
expect($e->filename)->toBe($filename);
3032
}
3133
});
34+
35+
it('Dont delete file if exist use setContainer', function () {
36+
$filename = __DIR__ . '/../tmp/test.csv';
37+
if (! file_exists($filename)) {
38+
file_put_contents($filename, 'test');
39+
}
40+
41+
$container = new Container();
42+
$container->singleton(DataExporter\Handler::CONTAINER_DATA_EXPORT_CONFIG_KEY, function () {
43+
return [
44+
'writer' => DataExporter::writerConfig(),
45+
'deleteFirstIfExist' => false,
46+
];
47+
});
48+
DataExporter::setContainer($container);
49+
50+
try {
51+
MyDataExport::csv([['a']])->saveAs($filename);
52+
} catch (FileAlreadyExistException $e) {
53+
expect($e->filename)->toBe($filename);
54+
}
55+
});

tests/Feature/Handler.php

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,39 @@
2424
$name = 'test';
2525
$builder = DataExporter::csv([['a']]);
2626
$realName = $builder->makeFilename($name);
27-
2827
expect($realName)->toBe('test.csv');
28+
29+
$name = __DIR__ . '/../test';
30+
$builder = DataExporter::csv([['a']]);
31+
$realName = $builder->makeFilename($name);
32+
expect($realName)->toBe(str_replace('\\', '/', dirname(__DIR__)) . '/test.csv');
33+
34+
// windows
35+
$name = 'G:\\\\abc\\test';
36+
$builder = DataExporter::csv([['a']]);
37+
$realName = $builder->makeFilename($name);
38+
expect($realName)->toBe('G://abc/test.csv');
39+
40+
// linux
41+
$name = '/abc/test';
42+
$builder = DataExporter::csv([['a']]);
43+
$realName = $builder->makeFilename($name);
44+
expect($realName)->toBe('/abc/test.csv');
45+
});
46+
47+
it('Handler makeFile can solve invalid character', function () {
48+
$name = 'abc /\\:*"<>|.csv?x=1';
49+
$builder = DataExporter::csv([['a']]);
50+
51+
$realName = $builder->makeFilename($name);
52+
expect($realName)->toBe('abc /_____.csv'); // / 被认为是路径分隔符,因此留下了
53+
54+
$realName = $builder->makeFilename($name, false);
55+
expect($realName)->toBe('abc _______.csv'); // 设置不含 path 后,/ 不会被认为是路径分隔符
56+
57+
$name = 'abc /\\:*"<>|?x=1';
58+
$builder = DataExporter::csv([['a']]);
59+
60+
$realName = $builder->makeFilename($name, false);
61+
expect($realName)->toBe('abc ________x=1.csv'); // 因为不存在 .csv 后缀在 name 中,因此 ? 会被保留替换
2962
});

tests/Feature/WriterTest.php

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
use Kriss\DataExporter\DataExporter;
44
use PhpOffice\PhpSpreadsheet\IOFactory;
5+
use Symfony\Component\Filesystem\Path;
56

67
beforeEach(function () {
78
$this->source = [
@@ -16,79 +17,79 @@
1617
it('Writer csv', function () {
1718
$filename = DataExporter::csv($this->source)->saveAs($this->filename);
1819

19-
expect($this->filename . '.csv')->toBe($filename);
20+
expect(Path::canonicalize($this->filename . '.csv'))->toBe($filename);
2021
$factory = IOFactory::load($filename);
2122
expect((string)$factory->getActiveSheet()->getCell('C4')->getValue())->toBe('cc');
2223
});
2324

2425
it('Writer xlsx', function () {
2526
$filename = DataExporter::xlsx($this->source)->saveAs($this->filename);
2627

27-
expect($this->filename . '.xlsx')->toBe($filename);
28+
expect(Path::canonicalize($this->filename . '.xlsx'))->toBe($filename);
2829
$factory = IOFactory::load($filename);
2930
expect((string)$factory->getActiveSheet()->getCell('C4')->getValue())->toBe('cc');
3031
});
3132

3233
it('Writer xls', function () {
3334
$filename = DataExporter::xls($this->source)->saveAs($this->filename);
3435

35-
expect($this->filename . '.xls')->toBe($filename);
36+
expect(Path::canonicalize($this->filename . '.xls'))->toBe($filename);
3637
$factory = IOFactory::load($filename);
3738
expect((string)$factory->getActiveSheet()->getCell('C4')->getValue())->toBe('cc');
3839
});
3940

4041
it('Writer csvSpout', function () {
4142
$filename = DataExporter::csvSpout($this->source)->saveAs($this->filename);
4243

43-
expect($this->filename . '.csv')->toBe($filename);
44+
expect(Path::canonicalize($this->filename . '.csv'))->toBe($filename);
4445
$factory = IOFactory::load($filename);
4546
expect((string)$factory->getActiveSheet()->getCell('C4')->getValue())->toBe('cc');
4647
});
4748

4849
it('Writer xlsxSpout', function () {
4950
$filename = DataExporter::xlsxSpout($this->source)->saveAs($this->filename);
5051

51-
expect($this->filename . '.xlsx')->toBe($filename);
52+
expect(Path::canonicalize($this->filename . '.xlsx'))->toBe($filename);
5253
$factory = IOFactory::load($filename);
5354
expect((string)$factory->getActiveSheet()->getCell('C4')->getValue())->toBe('cc');
5455
});
5556

5657
it('Writer odsSpout', function () {
5758
$filename = DataExporter::odsSpout($this->source)->saveAs($this->filename);
5859

59-
expect($this->filename . '.ods')->toBe($filename);
60+
expect(Path::canonicalize($this->filename . '.ods'))->toBe($filename);
6061
$factory = IOFactory::load($filename);
6162
expect((string)$factory->getActiveSheet()->getCell('C4')->getValue())->toBe('cc');
6263
});
6364

6465
it('Writer csvSpreadsheet', function () {
6566
$filename = DataExporter::csvSpreadsheet($this->source)->saveAs($this->filename);
6667

67-
expect($this->filename . '.csv')->toBe($filename);
68+
expect(Path::canonicalize($this->filename . '.csv'))->toBe($filename);
6869
$factory = IOFactory::load($filename);
6970
expect((string)$factory->getActiveSheet()->getCell('C4')->getValue())->toBe('cc');
7071
});
7172

7273
it('Writer xlsSpreadsheet', function () {
7374
$filename = DataExporter::xlsSpreadsheet($this->source)->saveAs($this->filename);
7475

75-
expect($this->filename . '.xls')->toBe($filename);
76+
expect(Path::canonicalize($this->filename . '.xls'))->toBe($filename);
7677
$factory = IOFactory::load($filename);
7778
expect((string)$factory->getActiveSheet()->getCell('C4')->getValue())->toBe('cc');
7879
});
7980

8081
it('Writer xlsxSpreadsheet', function () {
8182
$filename = DataExporter::xlsxSpreadsheet($this->source)->saveAs($this->filename);
8283

83-
expect($this->filename . '.xlsx')->toBe($filename);
84+
expect(Path::canonicalize($this->filename . '.xlsx'))->toBe($filename);
8485
$factory = IOFactory::load($filename);
8586
expect((string)$factory->getActiveSheet()->getCell('C4')->getValue())->toBe('cc');
8687
});
8788

8889
it('Writer odsSpreadsheet', function () {
8990
$filename = DataExporter::odsSpreadsheet($this->source)->saveAs($this->filename);
9091

91-
expect($this->filename . '.ods')->toBe($filename);
92+
expect(Path::canonicalize($this->filename . '.ods'))->toBe($filename);
9293
$factory = IOFactory::load($filename);
9394
expect((string)$factory->getActiveSheet()->getCell('C4')->getValue())->toBe('cc');
9495
});

tests/Unit/DataExporterTest.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
<?php
22

33
use Kriss\DataExporter\DataExporter;
4+
use Symfony\Component\Filesystem\Path;
45

56
it('DataExporter __callStatic result type correct', function () {
67
$exporter = DataExporter::xlsx([['a', 'b']]);
@@ -11,5 +12,5 @@
1112
$savePath = __DIR__ . '/../tmp/test';
1213
$savePath = DataExporter::xlsx([['a', 'b']])->saveAs($savePath);
1314

14-
expect($savePath)->toBe(__DIR__ . '/../tmp/test.xlsx');
15+
expect($savePath)->toBe(Path::canonicalize(__DIR__ . '/../tmp/test.xlsx'));
1516
});

0 commit comments

Comments
 (0)