diff --git a/.circleci/config.yml b/.circleci/config.yml index e608876..c5547a7 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -32,6 +32,8 @@ test: &test php composer-setup.php --quiet --install-dir /usr/local/bin --filename composer + - run: docker-php-ext-install bcmath + - run: composer update -n --prefer-dist $COMPOSER_FLAGS - save_cache: @@ -75,6 +77,8 @@ test_and_cover: &test_and_cover php composer-setup.php --quiet --install-dir /usr/local/bin --filename composer + - run: docker-php-ext-install bcmath + - run: composer update -n --prefer-dist - run: | @@ -125,6 +129,8 @@ code_fixer: &code_fixer php composer-setup.php --quiet --install-dir /usr/local/bin --filename composer + - run: docker-php-ext-install bcmath + - run: composer update -n --prefer-dist - save_cache: @@ -135,6 +141,49 @@ code_fixer: &code_fixer # run tests! - run: vendor/bin/php-cs-fixer fix --config=.php_cs.dist -v --dry-run --using-cache=no --path-mode=intersection -- src tests +build_phar: &build_phar + steps: + - checkout + + # Download and cache dependencies + - restore_cache: + keys: + - v2-test-dependencies-{{ .Environment.CIRCLE_JOB }}-{{ checksum "composer.json" }}-{{ checksum ".circleci/config.yml" }} + # fallback to using the latest cache if no exact match is found + - v2-test-dependencies- + + # php:* has no zip extension and the CLI is faster to install. + - run: apt-get update -y && apt-get install unzip -y + + - run: | + EXPECTED_SIGNATURE=$(curl -L https://composer.github.io/installer.sig) + php -r "copy('https://getcomposer.org/installer', 'composer-setup.php');" + ACTUAL_SIGNATURE=$(php -r "echo hash_file('SHA384', 'composer-setup.php');") + + if [ "$EXPECTED_SIGNATURE" != "$ACTUAL_SIGNATURE" ] + then + >&2 echo 'ERROR: Invalid installer signature' + rm composer-setup.php + exit 1 + fi + + php composer-setup.php --quiet --install-dir /usr/local/bin --filename composer + + - run: docker-php-ext-install bcmath + + - run: composer update -n --prefer-dist + + - save_cache: + paths: + - /root/.composer/cache/files + key: v2-test-dependencies-{{ .Environment.CIRCLE_JOB }}-{{ checksum "composer.json" }}-{{ checksum ".circleci/config.yml" }} + + # Build the phar! + - run: composer build-phar + + - store_artifacts: + path: ./har.phar + jobs: build_php73: docker: @@ -157,12 +206,20 @@ jobs: code_fixer: docker: - - image: php:7.2 + - image: php:7.3 working_directory: ~/repo <<: *code_fixer + build_phar: + docker: + - image: php:7.3 + + working_directory: ~/repo + + <<: *build_phar + workflows: version: 2 @@ -172,6 +229,9 @@ workflows: - build_php72 - build_php73 - code_fixer + - build_phar: + requires: + - build_php73 nightly: triggers: @@ -186,3 +246,6 @@ workflows: - build_php72 - build_php73 - code_fixer + - build_phar: + requires: + - build_php73 diff --git a/.gitignore b/.gitignore index 1843bc6..a399fca 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,4 @@ build .phpunit.result.cache vendor .php_cs.cache +har.phar diff --git a/README.md b/README.md index d3f2c1e..8081baa 100644 --- a/README.md +++ b/README.md @@ -20,6 +20,7 @@ Features include: * Writing a `\Deviantintegral\Har\Har` back out into a HAR JSON string. * Adapters for PSR-7 Request and Response interfaces. * An interface and `\Deviantintegral\Har\HarRepository` class to load HARs from a filesystem or other backend. +* A CLI tool to split a HAR file into single files per request / response pair. ## Example diff --git a/bin/console b/bin/console new file mode 100755 index 0000000..ec086a9 --- /dev/null +++ b/bin/console @@ -0,0 +1,16 @@ +#!/usr/bin/env php +add(new SplitCommand()); + +// Run it +$application->run(); diff --git a/composer.json b/composer.json index f27d451..addb6fd 100644 --- a/composer.json +++ b/composer.json @@ -4,6 +4,7 @@ "type": "library", "license": "GPL-2.0+", "minimum-stability": "dev", + "prefer-stable": true, "authors": [ { "name": "Andrew Berry", @@ -17,13 +18,14 @@ "doctrine/annotations": "^1.7", "guzzlehttp/psr7": "^1.6", "deviantintegral/jms-serializer-uri-handler": "^1.0", - "deviantintegral/null-date-time": "^0.1.0" + "deviantintegral/null-date-time": "^0.1.0", + "symfony/console": "^4.3" }, "require-dev": { "friendsofphp/php-cs-fixer": "^2.15", "phpunit/phpunit": "^8.3", - "symfony/console": "^4.3", - "guzzlehttp/guzzle": "^6.3" + "guzzlehttp/guzzle": "^6.3", + "macfja/phar-builder": "^0.2.8" }, "autoload": { "psr-4": { @@ -34,5 +36,17 @@ "psr-4": { "Deviantintegral\\Har\\Tests\\": "tests/src" } + }, + "scripts": { + "build-phar": "php -dphar.readonly=0 vendor/bin/phar-builder package composer.json && chmod +x har.phar" + }, + "extra": { + "phar-builder": { + "compression": "gzip", + "name": "har.phar", + "output-dir": "./", + "entry-point": "bin/console", + "include": [] + } } } diff --git a/src/Command/SplitCommand.php b/src/Command/SplitCommand.php new file mode 100644 index 0000000..c7a21b7 --- /dev/null +++ b/src/Command/SplitCommand.php @@ -0,0 +1,90 @@ +setName('har:split') + ->setDescription('Split a HAR file into one file per entry') + ->setHelp('Each entry in the supplied HAR will be split into a single file.') + ->addArgument('har', InputArgument::REQUIRED, 'The source HAR file to split.') + ->addArgument('destination', InputArgument::OPTIONAL, 'The source directory to save the split files to. Defaults to the current directory.') + ->addOption('md5', null, InputOption::VALUE_NONE, 'Save split files with an MD5 hash of the request URL instead of a numeric index.') + ->addOption('force', 'f', InputOption::VALUE_NONE, 'Overwrite destination files that already exist.'); + } + + protected function execute(InputInterface $input, OutputInterface $output) + { + $io = new SymfonyStyle($input, $output); + $source = $input->getArgument('har'); + $contents = file_get_contents($source); + $serializer = new Serializer(); + $har = $serializer->deserializeHar($contents); + + $io->text(sprintf('Splitting %s into one file per entry', + $source + )); + $io->progressStart(\count($har->getLog()->getEntries())); + + foreach ($har->splitLogEntries() as $index => $cloned) { + $destination = $this->getSplitDestination( + $index, + $input->getOption('md5'), + $cloned, + $input->getArgument('destination') ?: getcwd() + ); + + if ($input->getOption('force') || !file_exists($destination)) { + if (false === file_put_contents($destination, $serializer->serializeHar($cloned))) { + throw new \RuntimeException(sprintf('Unable to write to %s.', $destination)); + } + } else { + throw new \RuntimeException(sprintf('%s exists. Use --force to overwrite it and all other existing files', $destination)); + } + $io->progressAdvance(); + } + $io->progressFinish(); + } + + /** + * @param $index + * @param $md5 + * @param \Deviantintegral\Har\Har $cloned + * @param string $destination_path + * + * @return string + */ + private function getSplitDestination( + $index, + $md5, + \Deviantintegral\Har\Har $cloned, + string $destination_path + ): string { + $filename = $index + 1 .'.har'; + if ($md5) { + $filename = md5( + (string) $cloned->getLog()->getEntries()[0]->getRequest() + ->getUrl() + ).'.har'; + } + $destination = $destination_path."/$filename"; + + return $destination; + } +} diff --git a/src/Har.php b/src/Har.php index 547efa3..4ee914c 100644 --- a/src/Har.php +++ b/src/Har.php @@ -33,4 +33,18 @@ public function setLog(Log $log): self return $this; } + + /** + * Return a generator that returns cloned HARs with one per HAR entry. + * + * @return \Deviantintegral\Har\Har[] + */ + public function splitLogEntries(): \Generator + { + foreach ($this->getLog()->getEntries() as $index => $entry) { + $cloned = clone $this; + $cloned->getLog()->setEntries([$entry]); + yield $index => $cloned; + } + } }