diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 1783348..1a4ccec 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -1,4 +1,4 @@
-# This is the main Continuous Integration (CI) pipeline for the the stellarwp/plugin-framework package.
+# Continuous Integration (CI) pipeline.
#
# Any time code is pushed to one of the main branches or a PR is opened, this pipeline should be
# run to ensure everything still works as designed and meets our coding standards.
@@ -19,16 +19,52 @@ concurrency:
jobs:
+ # Check coding standards (PHP_CodeSniffer, PHP-CS-Fixer, Shellcheck)
+ coding-standards:
+ name: PHPCS
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+
+ - name: Configure PHP environment
+ uses: shivammathur/setup-php@v2
+ with:
+ php-version: '8.3'
+ extensions: mbstring
+ coverage: none
+
+ - uses: ramsey/composer-install@v2
+
+ - name: Run PHPCS
+ run: composer test:standards
+
+ # Static Code Analysis (PHPStan)
+ static-code-analysis:
+ name: PHPStan
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+
+ - name: Configure PHP environment
+ uses: shivammathur/setup-php@v2
+ with:
+ php-version: '8.3'
+ extensions: mbstring, intl
+ coverage: none
+
+ - uses: ramsey/composer-install@v2
+
+ - name: Run PHPStan
+ run: composer test:analysis
+
# Execute all PHPUnit tests.
phpunit:
- name: PHPUnit (PHP ${{ matrix.php-versions }}, WP ${{ matrix.wp-versions }})
+ name: PHPUnit (PHP ${{ matrix.php-versions }})
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
- # Run on all versions of PHP supported by WordPress.
php-versions: ['8.0', '8.1', '8.2', '8.3']
- wp-versions: ['latest']
services:
mysql:
@@ -40,7 +76,7 @@ jobs:
options: --health-cmd="mysqladmin ping" --health-interval=10s --health-timeout=10s --health-retries=10
steps:
- - uses: actions/checkout@v2
+ - uses: actions/checkout@v4
- name: Configure PHP environment
uses: shivammathur/setup-php@v2
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
new file mode 100644
index 0000000..8a81afe
--- /dev/null
+++ b/.github/workflows/release.yml
@@ -0,0 +1,49 @@
+# This workflow automates the release process: when a branch matching "release/vX.Y.Z" is merged
+# into "main", automatically create a new **draft** release with information about the release.
+name: Release
+
+# Only execute when a release branch has been merged into "main".
+on:
+ pull_request:
+ types:
+ - closed
+ branches:
+ - main
+
+jobs:
+
+ # Automatically prepare a release following a successful merge into "main".
+ publish-release:
+ if: github.event.pull_request.merged == true
+ name: Release to Packagist
+ runs-on: ubuntu-latest
+
+ steps:
+ - uses: actions/checkout@v4
+
+ - name: Determine release version
+ id: parse-version
+ env:
+ # Parse the version number from a branch name based on semver.
+ # Reference and examples of matched patterns: https://regexr.com/6jfqu
+ pattern: '(?:^|\/)v?\.?\K(\d+\.\d+\.\d+(-[0-9A-Za-z-]+(?:\.\d+)?)?(\+(?:\.?[0-9A-Za-z-]+)+)?)$'
+ run: |
+ version=$(grep -oP "${{ env.pattern }}" <<< "${{ github.event.pull_request.head.ref }}")
+ echo "::set-output name=version::$version"
+ echo "Parsed version: '${version}'"
+
+ - name: Create draft release
+ id: publish
+ uses: ncipollo/release-action@v1
+ with:
+ token: ${{ secrets.GITHUB_TOKEN }}
+ tag: "v${{ steps.parse-version.outputs.version }}"
+ commit: main
+ name: ${{ github.event.pull_request.title }}
+ body: ${{ github.event.pull_request.body }}
+ draft: true
+ prerelease: ${{ contains(steps.publish.outputs.version, '-') }}
+
+ - name: Release details
+ run: |
+ echo "Draft release created: ${{ steps.publish.outputs.html_url }}"
diff --git a/.gitignore b/.gitignore
index e6c97b0..f1a2791 100644
--- a/.gitignore
+++ b/.gitignore
@@ -16,6 +16,7 @@ composer.lock
.php-cs-fixer.php
phpcs.xml
phpunit.xml
+phpstan.neon
# Cache files.
.php_cs.cache
diff --git a/.phpcs.xml.dist b/.phpcs.xml.dist
new file mode 100644
index 0000000..eacdb67
--- /dev/null
+++ b/.phpcs.xml.dist
@@ -0,0 +1,29 @@
+
+
+
+
+
+ ./src
+ ./tests
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ tests/bootstrap.php
+
+
diff --git a/README.md b/README.md
index a862e62..876925d 100755
--- a/README.md
+++ b/README.md
@@ -12,6 +12,10 @@ Installation
-----------
It is recommended to [install the composer package](https://packagist.org/packages/renventura/wp-package-parser).
+```sh
+composer require renventura/wp-package-parser
+```
+
Basic usage
-----------
### Extract plugin metadata:
diff --git a/composer.json b/composer.json
index 5bedc49..dfa47ec 100755
--- a/composer.json
+++ b/composer.json
@@ -12,7 +12,9 @@
"erusev/parsedown": "^1.7"
},
"require-dev": {
- "phpunit/phpunit": "^9.6"
+ "phpunit/phpunit": "^9.6",
+ "phpstan/phpstan": "^1.11",
+ "squizlabs/php_codesniffer": "^3.10"
},
"autoload": {
"psr-4": {
@@ -25,18 +27,31 @@
}
},
"scripts": {
+ "fix:standards": [
+ "./vendor/bin/phpcbf ./src ./tests"
+ ],
"test": [
"@test:all"
],
"test:all": [
+ "@test:analysis",
+ "@test:standards",
"@test:unit"
],
+ "test:analysis": [
+ "./vendor/bin/phpstan analyse -c phpstan.neon.dist --memory-limit=768M"
+ ],
+ "test:standards": [
+ "./vendor/bin/phpcs ./src ./tests"
+ ],
"test:unit": [
"./vendor/bin/phpunit --testdox --verbose --color=always"
]
},
"scripts-descriptions": {
"test:all": "Run all automated tests.",
+ "test:analysis": "Perform static code analysis.",
+ "test:standards": "Check coding standards.",
"test:unit": "Run all of the PHPUnit test suites."
}
}
diff --git a/phpstan.neon.dist b/phpstan.neon.dist
new file mode 100644
index 0000000..d6663b8
--- /dev/null
+++ b/phpstan.neon.dist
@@ -0,0 +1,11 @@
+# Configuration for PHPStan
+# https://phpstan.org/config-reference
+
+parameters:
+ level: 6
+ bootstrapFiles:
+ - tests/bootstrap-phpstan.php
+ paths:
+ - src
+ - tests
+
diff --git a/src/Parsers/Parser.php b/src/Parsers/Parser.php
index 878670d..962a6a0 100755
--- a/src/Parsers/Parser.php
+++ b/src/Parsers/Parser.php
@@ -1,65 +1,69 @@
+ */
+ protected $headerMap = array();
- /**
- * Parse the file contents to retrieve its metadata.
- *
- * Searches for metadata for a file, such as a plugin or theme. Each piece of
- * metadata must be on its own line. For a field spanning multiple lines, it
- * must not have any newlines or only parts of it will be displayed.
- *
- * @param string $fileContents File contents. Can be safely truncated to 8kiB as that's all WP itself scans.
- *
- * @return array
- */
- protected function parseHeaders( string $fileContents ) : array {
- $headers = array();
- $headerMap = $this->headerMap;
+ /**
+ * Parse the file contents to retrieve its metadata.
+ *
+ * Searches for metadata for a file, such as a plugin or theme. Each piece of
+ * metadata must be on its own line. For a field spanning multiple lines, it
+ * must not have any newlines or only parts of it will be displayed.
+ *
+ * @param string $fileContents File contents. Can be safely truncated to 8kiB as that's all WP itself scans.
+ *
+ * @return array
+ */
+ protected function parseHeaders(string $fileContents): array
+ {
+ $headers = array();
+ $headerMap = $this->headerMap;
- // Support systems that use CR as a line ending.
- $fileContents = str_replace( "\r", "\n", $fileContents );
+ // Support systems that use CR as a line ending.
+ $fileContents = str_replace("\r", "\n", $fileContents);
- foreach ( $headerMap as $field => $prettyName ) {
- $found = preg_match( '/^[ \t\/*#@]*' . preg_quote( $prettyName, '/' ) . ':(.*)$/mi', $fileContents, $matches );
- if ( ( $found > 0 ) && ! empty( $matches[1] ) ) {
- // Strip comment markers and closing PHP tags.
- $value = trim( preg_replace( "/\s*(?:\*\/|\?>).*/", '', $matches[1] ) );
- $headers[ $field ] = $value;
- } else {
- $headers[ $field ] = '';
- }
- }
+ foreach ($headerMap as $field => $prettyName) {
+ $found = preg_match('/^[ \t\/*#@]*' . preg_quote($prettyName, '/') . ':(.*)$/mi', $fileContents, $matches);
+ if (( $found > 0 ) && ! empty($matches[1])) {
+ // Strip comment markers and closing PHP tags.
+ $value = trim(preg_replace("/\s*(?:\*\/|\?>).*/", '', $matches[1]));
+ $headers[ $field ] = $value;
+ } else {
+ $headers[ $field ] = '';
+ }
+ }
- return $headers;
- }
+ return $headers;
+ }
- /**
- * Transform Markdown markup to HTML.
- *
- * Tries (in vain) to emulate the transformation that WordPress.org applies to readme.txt files.
- *
- * @param string $text
- *
- * @return string
- */
- protected function applyMarkdown( string $text ) : string {
- // The WP standard for readme files uses some custom markup, like "= H4 headers ="
- $text = preg_replace( '@^\s*=\s*(.+?)\s*=\s*$@m', "$1
\n", $text );
- $markdown = new Parsedown();
- return $markdown->parse( $text );
- }
+ /**
+ * Transform Markdown markup to HTML.
+ *
+ * Tries (in vain) to emulate the transformation that WordPress.org applies to readme.txt files.
+ *
+ * @param string $text
+ *
+ * @return string
+ */
+ protected function applyMarkdown(string $text): string
+ {
+ // The WP standard for readme files uses some custom markup, like "= H4 headers ="
+ $text = preg_replace('@^\s*=\s*(.+?)\s*=\s*$@m', "$1
\n", $text);
+ $markdown = new Parsedown();
+ return $markdown->parse($text);
+ }
}
diff --git a/src/Parsers/PluginParser.php b/src/Parsers/PluginParser.php
index f886b7a..0c7c44d 100755
--- a/src/Parsers/PluginParser.php
+++ b/src/Parsers/PluginParser.php
@@ -1,167 +1,173 @@
'Plugin Name',
- 'plugin_uri' => 'Plugin URI',
- 'version' => 'Version',
- 'description' => 'Description',
- 'author' => 'Author',
- 'author_profile' => 'Author URI',
- 'text_domain' => 'Text Domain',
- 'domain_path' => 'Domain Path',
- 'network' => 'Network',
- );
-
- /**
- * Parse file readme.txt
- *
- * @param string $content Readme file content.
- *
- * @return array
- */
- public function parseReadme( string $content ) : array {
- $readmeTxtContents = trim( $content, " \t\n\r" );
- $readme = array(
- 'name' => '',
- 'contributors' => array(),
- 'donate' => '',
- 'tags' => array(),
- 'requires' => '',
- 'tested' => '',
- 'stable' => '',
- 'short_description' => '',
- 'sections' => array(),
- );
-
- // The readme.txt header has a fairly fixed structure, so we can parse it line-by-line
- $lines = explode( "\n", $readmeTxtContents );
-
- // Plugin name is at the very top, e.g. === My Plugin ===
- if ( preg_match( '@===\s*(.+?)\s*===@', array_shift( $lines ), $matches ) ) {
- $readme['name'] = $matches[1];
- } else {
- return null;
- }
-
- // Then there's a bunch of meta fields formatted as "Field: value"
- $headers = array();
- $headerMap = array(
- 'Contributors' => 'contributors',
- 'Donate link' => 'donate',
- 'Tags' => 'tags',
- 'Requires at least' => 'requires',
- 'Tested up to' => 'tested',
- 'Stable tag' => 'stable',
- );
- do { // Parse each readme.txt header
- $pieces = explode( ':', array_shift( $lines ), 2 );
- if ( array_key_exists( $pieces[0], $headerMap ) ) {
- if ( isset( $pieces[1] ) ) {
- $headers[ $headerMap[ $pieces[0] ] ] = trim( $pieces[1] );
- } else {
- $headers[ $headerMap[ $pieces[0] ] ] = '';
- }
- }
- } while ( trim( $pieces[0] ) != '' ); // Until an empty line is encountered
-
- // "Contributors" is a comma-separated list. Convert it to an array.
- if ( ! empty( $headers['contributors'] ) ) {
- $headers['contributors'] = array_map( 'trim', explode( ',', $headers['contributors'] ) );
- }
-
- // Likewise for "Tags"
- if ( ! empty( $headers['tags'] ) ) {
- $headers['tags'] = array_map( 'trim', explode( ',', $headers['tags'] ) );
- }
-
- $readme = array_merge( $readme, $headers );
-
- // After the headers comes the short description
- $readme['short_description'] = array_shift( $lines );
-
- // Finally, a valid readme.txt also contains one or more "sections" identified by "== Section Name =="
- $sections = array();
- $contentBuffer = array();
- $currentSection = '';
- foreach ( $lines as $line ) {
- // Is this a section header?
- if ( preg_match( '@^\s*==\s+(.+?)\s+==\s*$@m', $line, $matches ) ) {
-
+class PluginParser extends Parser
+{
+ /**
+ * Header map.
+ *
+ * @var array
+ */
+ protected $headerMap = array(
+ 'name' => 'Plugin Name',
+ 'plugin_uri' => 'Plugin URI',
+ 'description' => 'Description',
+ 'version' => 'Version',
+ 'requires_at_least' => 'Requires at least',
+ 'requires_php' => 'Requires PHP',
+ 'author' => 'Author',
+ 'author_profile' => 'Author URI',
+ 'license' => 'License',
+ 'license_uri' => 'License URI',
+ 'text_domain' => 'Text Domain',
+ 'domain_path' => 'Domain Path',
+ 'network' => 'Network',
+ 'update_uri' => 'Update URI',
+ 'requires_plugins' => 'Requires Plugins',
+ );
+
+ /**
+ * Parse file readme.txt
+ *
+ * @param string $content Readme file content.
+ *
+ * @return null|array
+ */
+ public function parseReadme(string $content): null|array
+ {
+ $readmeTxtContents = trim($content, " \t\n\r");
+ $readme = array(
+ 'name' => '',
+ 'contributors' => array(),
+ 'donate' => '',
+ 'tags' => array(),
+ 'requires' => '',
+ 'tested' => '',
+ 'stable' => '',
+ 'short_description' => '',
+ 'sections' => array(),
+ );
+
+ // The readme.txt header has a fairly fixed structure, so we can parse it line-by-line
+ $lines = explode("\n", $readmeTxtContents);
+
+ // Plugin name is at the very top, e.g. === My Plugin ===
+ if (preg_match('@===\s*(.+?)\s*===@', array_shift($lines), $matches)) {
+ $readme['name'] = $matches[1];
+ } else {
+ return [];
+ }
+
+ // Then there's a bunch of meta fields formatted as "Field: value"
+ $headers = array();
+ $headerMap = array(
+ 'Contributors' => 'contributors',
+ 'Donate link' => 'donate',
+ 'Tags' => 'tags',
+ 'Requires at least' => 'requires',
+ 'Tested up to' => 'tested',
+ 'Stable tag' => 'stable',
+ );
+ do { // Parse each readme.txt header
+ $pieces = explode(':', array_shift($lines), 2);
+ if (array_key_exists($pieces[0], $headerMap)) {
+ if (isset($pieces[1])) {
+ $headers[ $headerMap[ $pieces[0] ] ] = trim($pieces[1]);
+ } else {
+ $headers[ $headerMap[ $pieces[0] ] ] = '';
+ }
+ }
+ } while (trim($pieces[0]) != ''); // Until an empty line is encountered
+
+ // "Contributors" is a comma-separated list. Convert it to an array.
+ if (! empty($headers['contributors'])) {
+ $headers['contributors'] = array_map('trim', explode(',', $headers['contributors']));
+ }
+
+ // Likewise for "Tags"
+ if (! empty($headers['tags'])) {
+ $headers['tags'] = array_map('trim', explode(',', $headers['tags']));
+ }
+
+ $readme = array_merge($readme, $headers);
+
+ // After the headers comes the short description
+ $readme['short_description'] = array_shift($lines);
+
+ // Finally, a valid readme.txt also contains one or more "sections" identified by "== Section Name =="
+ $sections = array();
+ $contentBuffer = array();
+ $currentSection = '';
+ foreach ($lines as $line) {
+ // Is this a section header?
+ if (preg_match('@^\s*==\s+(.+?)\s+==\s*$@m', $line, $matches)) {
// Flush the content buffer for the previous section, if any
- if ( ! empty( $currentSection ) ) {
- $sectionContent = trim( implode( "\n", $contentBuffer ) );
- $sections[ $currentSection ] = $sectionContent;
- }
-
- // Start reading a new section
- $currentSection = $matches[1];
- $currentSection = strtolower($currentSection);
- $contentBuffer = array();
-
- } else {
-
- // Buffer all section content
- $contentBuffer[] = $line;
- }
- }
-
- // Flush the buffer for the last section
- if ( ! empty( $currentSection ) ) {
- $sections[ $currentSection ] = trim( implode( "\n", $contentBuffer ) );
- }
-
- // Apply Markdown to sections
- $sections = array_map( array( $this, 'applyMarkdown' ), $sections );
- $readme['sections'] = $sections;
-
- return $readme;
- }
-
- /**
- * Parse the plugin contents to retrieve plugin's metadata headers.
- *
- * Adapted from the get_plugin_data() function used by WordPress.
- * Returns an array that contains the following:
- * 'Name' - Name of the plugin.
- * 'Title' - Title of the plugin and the link to the plugin's web site.
- * 'Description' - Description of what the plugin does and/or notes from the author.
- * 'Author' - The author's name.
- * 'AuthorURI' - The author's web site address.
- * 'Version' - The plugin version number.
- * 'PluginURI' - Plugin web site address.
- * 'TextDomain' - Plugin's text domain for localization.
- * 'DomainPath' - Plugin's relative directory path to .mo files.
- * 'Network' - Boolean. Whether the plugin can only be activated network wide.
- *
- * If the input string doesn't appear to contain a valid plugin header, the function
- * will return NULL.
- *
- * @param string $fileContents Contents of the plugin file
- *
- * @return array|null See above for description.
- */
- public function parsePlugin( string $fileContents ) : array|null {
-
- $headers = $this->parseHeaders( $fileContents );
-
- $headers['network'] = ( strtolower( $headers['network'] ) === 'true' );
-
- // If it doesn't have a name, it's probably not a plugin.
- if ( empty( $headers['name'] ) ) {
- return null;
- }
-
- return $headers;
- }
+ if (! empty($currentSection)) {
+ $sectionContent = trim(implode("\n", $contentBuffer));
+ $sections[ $currentSection ] = $sectionContent;
+ }
+
+ // Start reading a new section
+ $currentSection = $matches[1];
+ $currentSection = strtolower($currentSection);
+ $contentBuffer = array();
+ } else {
+ // Buffer all section content
+ $contentBuffer[] = $line;
+ }
+ }
+
+ // Flush the buffer for the last section
+ if (! empty($currentSection)) {
+ $sections[ $currentSection ] = trim(implode("\n", $contentBuffer));
+ }
+
+ // Apply Markdown to sections
+ $sections = array_map(array( $this, 'applyMarkdown' ), $sections);
+ $readme['sections'] = $sections;
+
+ return $readme;
+ }
+
+ /**
+ * Parse the plugin contents to retrieve plugin's metadata headers.
+ *
+ * Adapted from the get_plugin_data() function used by WordPress.
+ * Returns an array that contains the following:
+ * 'Name' - Name of the plugin.
+ * 'Title' - Title of the plugin and the link to the plugin's web site.
+ * 'Description' - Description of what the plugin does and/or notes from the author.
+ * 'Author' - The author's name.
+ * 'AuthorURI' - The author's web site address.
+ * 'Version' - The plugin version number.
+ * 'PluginURI' - Plugin web site address.
+ * 'TextDomain' - Plugin's text domain for localization.
+ * 'DomainPath' - Plugin's relative directory path to .mo files.
+ * 'Network' - Boolean. Whether the plugin can only be activated network wide.
+ *
+ * If the input string doesn't appear to contain a valid plugin header, the function
+ * will return NULL.
+ *
+ * @param string $fileContents Contents of the plugin file
+ *
+ * @return null|array See above for description.
+ */
+ public function parsePlugin(string $fileContents): array|null
+ {
+
+ $headers = $this->parseHeaders($fileContents);
+
+ $headers['network'] = ( strtolower($headers['network']) === 'true' );
+
+ // If it doesn't have a name, it's probably not a plugin.
+ if (empty($headers['name'])) {
+ return null;
+ }
+
+ return $headers;
+ }
}
diff --git a/src/Parsers/ThemeParser.php b/src/Parsers/ThemeParser.php
index 1207ce3..5f3a919 100755
--- a/src/Parsers/ThemeParser.php
+++ b/src/Parsers/ThemeParser.php
@@ -1,47 +1,49 @@
'Theme Name',
- 'theme_uri' => 'Theme URI',
- 'description' => 'Description',
- 'author' => 'Author',
- 'author_uri' => 'Author URI',
- 'version' => 'Version',
- 'template' => 'Template',
- 'status' => 'Status',
- 'tags' => 'Tags',
- 'text_domain' => 'Text Domain',
- 'domain_path' => 'Domain Path',
- );
+class ThemeParser extends Parser
+{
+ /**
+ * Header map.
+ *
+ * @var array
+ */
+ protected $headerMap = array(
+ 'name' => 'Theme Name',
+ 'theme_uri' => 'Theme URI',
+ 'description' => 'Description',
+ 'author' => 'Author',
+ 'author_uri' => 'Author URI',
+ 'version' => 'Version',
+ 'template' => 'Template',
+ 'status' => 'Status',
+ 'tags' => 'Tags',
+ 'text_domain' => 'Text Domain',
+ 'domain_path' => 'Domain Path',
+ );
- /**
- * Parse style.css file.
- *
- * @param string $fileContents Contents of style.css file.
- *
- * @return array|null
- */
- public function parseStyle( string $fileContents ) : array|null {
+ /**
+ * Parse style.css file.
+ *
+ * @param string $fileContents Contents of style.css file.
+ *
+ * @return null|array
+ */
+ public function parseStyle(string $fileContents): null|array
+ {
- $headers = $this->parseHeaders( $fileContents );
- $headers['tags'] = array_filter( array_map( 'trim', explode( ',', strip_tags( $headers['tags'] ) ) ) );
+ $headers = $this->parseHeaders($fileContents);
+ $headers['tags'] = array_filter(array_map('trim', explode(',', strip_tags($headers['tags']))));
- // If it doesn't have a name, it's probably not a valid theme.
- if ( empty( $headers['name'] ) ) {
- return null;
- }
+ // If it doesn't have a name, it's probably not a valid theme.
+ if (empty($headers['name'])) {
+ return null;
+ }
- return $headers;
- }
+ return $headers;
+ }
}
diff --git a/src/WPPackage.php b/src/WPPackage.php
index 59c352e..343ed44 100755
--- a/src/WPPackage.php
+++ b/src/WPPackage.php
@@ -1,207 +1,217 @@
package_path = $package_path;
- $this->parse();
- }
-
- /**
- * Get slug.
- *
- * @return string|null
- */
- public function getSlug() : string|null {
- $metadata = $this->getMetaData();
-
- if ( ! isset( $metadata['slug'] ) ) {
- return null;
- }
-
- return $metadata['slug'];
- }
-
- /**
- * Get metadata.
- *
- * @return array
- */
- public function getMetaData() : array {
- return $this->metadata;
- }
-
- /**
- * Parse package.
- *
- * @return bool
- */
- private function parse() : bool {
- if ( ! $this->validateFile() ) {
- return false;
- }
-
- $plugin_parser = new Parsers\PluginParser();
- $theme_parser = new Parsers\ThemeParser();
-
- $slug = null;
- $zip = $this->openPackage();
- $files = $zip->numFiles;
-
- for ( $index = 0; $index < $files; $index ++ ) {
- $info = $zip->statIndex( $index );
-
- $file = $this->exploreFile( $info['name'] );
- if ( ! $file ) {
- continue;
- }
-
- $slug = $file['dirname'];
- $file_name = $file['name'] . '.' . $file['extension'];
- $content = $zip->getFromIndex( $index );
-
- if ( $file['extension'] === 'php' ) {
- $headers = $plugin_parser->parsePlugin( $content );
-
- if ( $headers ) {
- //Add plugin file
- $plugin_file = $slug . '/' . $file_name;
- $headers['plugin'] = $plugin_file;
-
- $this->type = 'plugin';
- $this->metadata = array_merge( $this->metadata, $headers );
- }
-
- continue;
- }
-
- if ( $file_name === 'readme.txt' ) {
- $data = $plugin_parser->parseReadme( $content );
- unset( $data['name'] );
- $data['readme'] = true;
- $this->metadata = array_merge( $data, $this->metadata );
-
- continue;
- }
-
- if ( $file_name === 'style.css' ) {
- $headers = $theme_parser->parseStyle( $content );
- if ( $headers ) {
- $this->type = 'theme';
- $this->metadata = $headers;
- }
- }
- }
-
- if ( empty( $this->type ) ) {
- $this->metadata = array();
-
- return false;
- }
-
- $this->metadata['slug'] = $slug;
-
- return true;
- }
-
- /**
- * Get package type.
- *
- * @return string|null
- */
- public function getType() : string|null {
- return $this->type;
- }
-
- /**
- * Explore file.
- *
- * @param string $file_name File name.
- *
- * @return bool|array
- */
- private function exploreFile( string $file_name ) : bool|array {
- $data = pathinfo( $file_name );
- $dirname = $data['dirname'];
- $depth = substr_count( $dirname, '/' );
- $extension = ! empty( $data['extension'] ) ? $data['extension'] : false;
-
- //Skip directories and everything that's more than 1 sub-directory deep.
- if ( $depth > 0 || ! $extension ) {
- return false;
- }
-
- return array(
- 'dirname' => $dirname,
- 'name' => $data['filename'],
- 'extension' => $data['extension']
- );
- }
-
- /**
- * Validate package file.
- *
- * @return bool
- */
- private function validateFile() {
- $file = $this->package_path;
-
- if ( ! file_exists( $file ) || ! is_readable( $file ) ) {
- return false;
- }
-
- if ( 'zip' !== pathinfo( $file, PATHINFO_EXTENSION ) ) {
- return false;
- }
-
- return true;
- }
-
- /**
- * Open package file.
- *
- * @return false|ZipArchive
- */
- private function openPackage() : bool|ZipArchive {
- $file = $this->package_path;
-
- $zip = new ZipArchive();
- if ( $zip->open( $file ) !== true ) {
- return false;
- }
-
- return $zip;
- }
+class WPPackage
+{
+ /**
+ * Metadata.
+ *
+ * @var array
+ */
+ protected $metadata = array();
+
+ /**
+ * Package file path.
+ *
+ * @var string
+ */
+ private $package_path;
+
+ /**
+ * Package type.
+ *
+ * @var string
+ */
+ private $type = null;
+
+ /**
+ * Construct a package instance and parse the provided zip file.
+ *
+ * @param $package_path
+ */
+ public function __construct(string $package_path)
+ {
+ $this->package_path = $package_path;
+ $this->parse();
+ }
+
+ /**
+ * Get slug.
+ *
+ * @return string|null
+ */
+ public function getSlug(): string|null
+ {
+ $metadata = $this->getMetaData();
+
+ if (! isset($metadata['slug'])) {
+ return null;
+ }
+
+ return $metadata['slug'];
+ }
+
+ /**
+ * Get metadata.
+ *
+ * @return array
+ */
+ public function getMetaData(): array
+ {
+ return $this->metadata;
+ }
+
+ /**
+ * Parse package.
+ *
+ * @return bool
+ */
+ private function parse(): bool
+ {
+ if (! $this->validateFile()) {
+ return false;
+ }
+
+ $plugin_parser = new Parsers\PluginParser();
+ $theme_parser = new Parsers\ThemeParser();
+
+ $slug = null;
+ $zip = $this->openPackage();
+ $files = $zip->numFiles;
+
+ for ($index = 0; $index < $files; $index++) {
+ $info = $zip->statIndex($index);
+
+ $file = $this->exploreFile($info['name']);
+ if (! $file) {
+ continue;
+ }
+
+ $slug = $file['dirname'];
+ $file_name = $file['name'] . '.' . $file['extension'];
+ $content = $zip->getFromIndex($index);
+
+ if ($file['extension'] === 'php') {
+ $headers = $plugin_parser->parsePlugin($content);
+
+ if ($headers) {
+ //Add plugin file
+ $plugin_file = $slug . '/' . $file_name;
+ $headers['plugin'] = $plugin_file;
+
+ $this->type = 'plugin';
+ $this->metadata = array_merge($this->metadata, $headers);
+ }
+
+ continue;
+ }
+
+ if ($file_name === 'readme.txt') {
+ $data = $plugin_parser->parseReadme($content);
+ unset($data['name']);
+ $data['readme'] = true;
+ $this->metadata = array_merge($data, $this->metadata);
+
+ continue;
+ }
+
+ if ($file_name === 'style.css') {
+ $headers = $theme_parser->parseStyle($content);
+ if ($headers) {
+ $this->type = 'theme';
+ $this->metadata = $headers;
+ }
+ }
+ }
+
+ if (empty($this->type)) {
+ $this->metadata = array();
+
+ return false;
+ }
+
+ $this->metadata['slug'] = $slug;
+
+ return true;
+ }
+
+ /**
+ * Get package type.
+ *
+ * @return string|null
+ */
+ public function getType(): string|null
+ {
+ return $this->type;
+ }
+
+ /**
+ * Explore file.
+ *
+ * @param string $file_name File name.
+ *
+ * @return bool|array
+ */
+ private function exploreFile(string $file_name): bool|array
+ {
+ $data = pathinfo($file_name);
+ $dirname = $data['dirname'];
+ $depth = substr_count($dirname, '/');
+ $extension = ! empty($data['extension']) ? $data['extension'] : false;
+
+ //Skip directories and everything that's more than 1 sub-directory deep.
+ if ($depth > 0 || ! $extension) {
+ return false;
+ }
+
+ return array(
+ 'dirname' => $dirname,
+ 'name' => $data['filename'],
+ 'extension' => $data['extension']
+ );
+ }
+
+ /**
+ * Validate package file.
+ *
+ * @return bool
+ */
+ private function validateFile()
+ {
+ $file = $this->package_path;
+
+ if (! file_exists($file) || ! is_readable($file)) {
+ return false;
+ }
+
+ if ('zip' !== pathinfo($file, PATHINFO_EXTENSION)) {
+ return false;
+ }
+
+ return true;
+ }
+
+ /**
+ * Open package file.
+ *
+ * @return false|ZipArchive
+ */
+ private function openPackage(): bool|ZipArchive
+ {
+ $file = $this->package_path;
+
+ $zip = new ZipArchive();
+ if ($zip->open($file) !== true) {
+ return false;
+ }
+
+ return $zip;
+ }
}
diff --git a/tests/Unit/Parsers/PluginParserTest.php b/tests/Unit/Parsers/PluginParserTest.php
index 68f2d50..b118409 100755
--- a/tests/Unit/Parsers/PluginParserTest.php
+++ b/tests/Unit/Parsers/PluginParserTest.php
@@ -1,41 +1,52 @@
assertEquals( null, $package->getType() );
- $this->assertEquals( null, $package->getSlug() );
- $this->assertEquals( array(), $package->getMetaData() );
- }
+ *
+ * @return void
+ */
+ public function testNoInfoWhenPackageNotFound()
+ {
+ $package = new WPPackage('/path/wrong/abc.zip');
+ $this->assertEquals(null, $package->getType());
+ $this->assertEquals(null, $package->getSlug());
+ $this->assertEquals(array(), $package->getMetaData());
+ }
- /**
+ /**
* Correctly parses a valid plugin.
- */
- public function test_parses_valid_plugin() {
- $package = new WPPackage( TESTS_DIR . '/packages/hello-dolly.1.6.zip' );
- $this->assertEquals( 'plugin', $package->getType() );
- $this->assertEquals( 'hello-dolly', $package->getSlug() );
- }
+ *
+ * @return void
+ */
+ public function testParsesValidPlugin()
+ {
+ $package = new WPPackage(TESTS_DIR . '/packages/hello-dolly.1.6.zip');
+ $this->assertEquals('plugin', $package->getType());
+ $this->assertEquals('hello-dolly', $package->getSlug());
+ }
- /**
+ /**
* getMetaData() should return correct data about the package.
- */
- public function test_getMetaData_should_return_correct_data_for_plugin() {
- $package = new WPPackage( TESTS_DIR . '/packages/hello-dolly.1.6.zip' );
+ *
+ * @return void
+ */
+ public function testGetmetadataShouldReturnCorrectDataForPlugin()
+ {
+ $package = new WPPackage(TESTS_DIR . '/packages/hello-dolly.1.6.zip');
- $metadata = $package->getMetaData();
- $this->assertEquals( 'Hello Dolly', $metadata['name'] );
- $this->assertEquals( 'hello-dolly/hello.php', $metadata['plugin'] );
- $this->assertEquals( '4.6', $metadata['requires'] );
- $this->assertEquals( '4.7', $metadata['tested'] );
- $this->assertEquals( '1.6', $metadata['version'] );
- $this->assertEquals( 'hello-dolly', $metadata['slug'] );
- }
+ $metadata = $package->getMetaData();
+ $this->assertEquals('Hello Dolly', $metadata['name']);
+ $this->assertEquals('hello-dolly/hello.php', $metadata['plugin']);
+ $this->assertEquals('4.6', $metadata['requires']);
+ $this->assertEquals('4.7', $metadata['tested']);
+ $this->assertEquals('1.6', $metadata['version']);
+ $this->assertEquals('hello-dolly', $metadata['slug']);
+ }
}
diff --git a/tests/Unit/Parsers/ThemeParserTest.php b/tests/Unit/Parsers/ThemeParserTest.php
index fef7d59..6b5f29b 100755
--- a/tests/Unit/Parsers/ThemeParserTest.php
+++ b/tests/Unit/Parsers/ThemeParserTest.php
@@ -1,41 +1,52 @@
assertEquals( null, $package->getType() );
- $this->assertEquals( array(), $package->getMetaData() );
- $this->assertEquals( null, $package->getSlug() );
- }
+ *
+ * @return void
+ */
+ public function testNoInfoWhenPackageNotFound()
+ {
+ $package = new WPPackage('path/wrong/test.zip');
+ $this->assertEquals(null, $package->getType());
+ $this->assertEquals(array(), $package->getMetaData());
+ $this->assertEquals(null, $package->getSlug());
+ }
/**
* Correctly parses a valid package.
- */
- public function test_parses_valid_theme() {
- $package = new WPPackage( TESTS_DIR . '/packages/twentyseventeen.1.3.zip' );
- $this->assertEquals( 'theme', $package->getType() );
- $this->assertEquals( 'twentyseventeen', $package->getSlug() );
+ *
+ * @return void
+ */
+ public function testParsesValidTheme()
+ {
+ $package = new WPPackage(TESTS_DIR . '/packages/twentyseventeen.1.3.zip');
+ $this->assertEquals('theme', $package->getType());
+ $this->assertEquals('twentyseventeen', $package->getSlug());
}
- /**
+ /**
* getMetaData() should return correct data about the package.
+ *
+ * @return void
*/
- public function test_getMetaData_should_return_correct_data_for_theme() {
- $package = new WPPackage( TESTS_DIR . '/packages/twentysixteen.1.3.zip' );
- $this->assertEquals( 'theme', $package->getType() );
- $this->assertEquals( 'twentysixteen', $package->getSlug() );
+ public function testGetmetadataShouldReturnCorrectDataForTheme()
+ {
+ $package = new WPPackage(TESTS_DIR . '/packages/twentysixteen.1.3.zip');
+ $this->assertEquals('theme', $package->getType());
+ $this->assertEquals('twentysixteen', $package->getSlug());
- $metadata = $package->getMetaData();
- $this->assertEquals( '1.3', $metadata['version'] );
- $this->assertEquals( 'Twenty Sixteen', $metadata['name'] );
- $this->assertEquals( 'twentysixteen', $metadata['text_domain'] );
- $this->assertEquals( 'twentysixteen', $metadata['slug'] );
- }
+ $metadata = $package->getMetaData();
+ $this->assertEquals('1.3', $metadata['version']);
+ $this->assertEquals('Twenty Sixteen', $metadata['name']);
+ $this->assertEquals('twentysixteen', $metadata['text_domain']);
+ $this->assertEquals('twentysixteen', $metadata['slug']);
+ }
}
diff --git a/tests/bootstrap-phpstan.php b/tests/bootstrap-phpstan.php
new file mode 100644
index 0000000..95157a5
--- /dev/null
+++ b/tests/bootstrap-phpstan.php
@@ -0,0 +1,10 @@
+