├── .gitignore ├── CHANGELOG.md ├── LICENSE.txt ├── README.md ├── UPGRADE.md ├── bin ├── civix └── civix.bat ├── box.json ├── composer.json ├── composer.lock ├── doc ├── develop.md └── download.md ├── lib ├── README.md ├── civimix-schema@5.85.beta1.phar └── pathload-0.php ├── mixin-backports.php ├── mixin-mods ├── README.md └── setting-admin@1.0.1.mixin.php ├── nix ├── buildkit-update.sh └── buildkit.nix ├── phpunit.xml.dist ├── scoper.inc.php ├── scripts ├── build.sh ├── check-phar.php ├── load-snapshot.sh ├── make-snapshots.sh ├── releaser.php ├── run-tests.sh ├── tidy-snapshots.sh └── upgrade-snapshots.sh ├── shell.nix ├── src ├── CRM │ └── CivixBundle │ │ ├── Application.php │ │ ├── Builder.php │ │ ├── Builder │ │ ├── Collection.php │ │ ├── Content.php │ │ ├── CopyClass.php │ │ ├── CopyFile.php │ │ ├── CustomDataXML.php │ │ ├── Dirs.php │ │ ├── Info.php │ │ ├── Ini.php │ │ ├── License.php │ │ ├── Menu.php │ │ ├── Mixins.php │ │ ├── Module.php │ │ ├── PHPUnitGenerateInitFiles.php │ │ ├── PhpData.php │ │ ├── PhpUnitXML.php │ │ ├── Template.php │ │ └── XML.php │ │ ├── Checker.php │ │ ├── CivixTestListener.php │ │ ├── Command │ │ ├── AbstractAddPageCommand.php │ │ ├── AbstractCommand.php │ │ ├── AddAngularDirectiveCommand.php │ │ ├── AddAngularModuleCommand.php │ │ ├── AddAngularPageCommand.php │ │ ├── AddApiCommand.php │ │ ├── AddCaseTypeCommand.php │ │ ├── AddCustomDataCommand.php │ │ ├── AddEntityCommand.php │ │ ├── AddFormCommand.php │ │ ├── AddManagedEntityCommand.php │ │ ├── AddPageCommand.php │ │ ├── AddReportCommand.php │ │ ├── AddSearchCommand.php │ │ ├── AddServiceCommand.php │ │ ├── AddTestCommand.php │ │ ├── AddThemeCommand.php │ │ ├── AddUpgraderCommand.php │ │ ├── BuildCommand.php │ │ ├── ConfigGetCommand.php │ │ ├── ConfigSetCommand.php │ │ ├── ConvertEntityCommand.php │ │ ├── InfoGetCommand.php │ │ ├── InfoSetCommand.php │ │ ├── InitCommand.php │ │ ├── InspectFunctionCommand.php │ │ ├── Mgd.php │ │ ├── MixinCommand.php │ │ ├── PingCommand.php │ │ ├── TestRunCommand.php │ │ └── UpgradeCommand.php │ │ ├── ComposerCompile.php │ │ ├── Generator.php │ │ ├── Parse │ │ ├── ParseException.php │ │ ├── PrimitiveFunctionVisitor.php │ │ ├── PrimitiveFunctionVisitorTest.php │ │ └── Token.php │ │ ├── Resources │ │ ├── doc │ │ │ └── index.rst │ │ ├── translations │ │ │ └── messages.fr.xlf │ │ └── views │ │ │ └── Code │ │ │ ├── angular-dir.html.php │ │ │ ├── angular-dir.js.php │ │ │ ├── angular-module.css.php │ │ │ ├── angular-module.js.php │ │ │ ├── angular-page.hlp.php │ │ │ ├── angular-page.html.php │ │ │ ├── angular-page.js.php │ │ │ ├── api.php.php │ │ │ ├── bootstrap.css.php │ │ │ ├── case-type.xml.php │ │ │ ├── civicrm.css.php │ │ │ ├── entity-api.php.php │ │ │ ├── entity-api3-test.php.php │ │ │ ├── entity-api4.php.php │ │ │ ├── entity-bao.php.php │ │ │ ├── entity-dao.php.php │ │ │ ├── form.php.php │ │ │ ├── form.tpl.php │ │ │ ├── module.civix.php.php │ │ │ ├── module.php.php │ │ │ ├── page.php.php │ │ │ ├── page.tpl.php │ │ │ ├── phpunit-boot-cv.php.php │ │ │ ├── phpunit-boot.php.php │ │ │ ├── readme.md.php │ │ │ ├── report.php.php │ │ │ ├── report.tpl.php │ │ │ ├── search.php.php │ │ │ ├── service.php.php │ │ │ ├── test-api.php.php │ │ │ ├── test-e2e.php.php │ │ │ ├── test-headless.php.php │ │ │ ├── test-legacy.php.php │ │ │ ├── test-phpunit.php.php │ │ │ ├── upgrader-base.php.php │ │ │ └── upgrader.php.php │ │ ├── RunMethodsTrait.php │ │ ├── SkippedMethodException.php │ │ ├── Test │ │ ├── CommandTester.php │ │ └── SubProcessCommandTester.php │ │ ├── UpgradeList.php │ │ └── Utils │ │ ├── AutoCleanup.php │ │ ├── CivixStyle.php │ │ ├── Commands.php │ │ ├── EvilEx.php │ │ ├── Files.php │ │ ├── Formatting.php │ │ ├── Functions.php │ │ ├── IOStack.php │ │ ├── MixinLibraries.php │ │ ├── Naming.php │ │ ├── NamingTest.php │ │ ├── Path.php │ │ ├── PathTest.php │ │ ├── PathloadPackage.php │ │ ├── SchemaBackport.php │ │ └── Versioning.php └── Civix.php ├── tests ├── TODO.md ├── bootstrap.php ├── e2e │ ├── AddEntityTest.php │ ├── AddManagedEntityTest.php │ ├── AddPageTest.php │ ├── AddServiceTest.php │ ├── CRMNamingTest.php │ ├── CiviNamingTest.php │ ├── CivixProjectTestTrait.php │ ├── CivixSnapshotUpgradeTestTrait.php │ ├── CleanEmptyTest.in.txt │ ├── CleanEmptyTest.out.txt │ ├── CleanEmptyTest.php │ ├── IdempotentUpgradeTest.php │ ├── MixinMgmtTest.php │ ├── MultiExtensionTest.php │ └── SnapshotUpgradeTest.php ├── scenarios │ ├── README.md │ ├── empty │ │ └── make.sh │ ├── entity4 │ │ ├── Entity4Test.php │ │ ├── bootstrap.php │ │ ├── make.sh │ │ └── phpunit.xml.dist │ ├── kitchensink │ │ └── make.sh │ ├── qf │ │ └── make.sh │ └── svc │ │ └── make.sh └── snapshots │ ├── org.example.civixsnapshot-v16.02.0-empty │ └── original.zip │ ├── org.example.civixsnapshot-v16.02.0-kitchensink │ └── original.zip │ ├── org.example.civixsnapshot-v16.02.0-qf │ └── original.zip │ ├── org.example.civixsnapshot-v17.10.5-empty │ └── original.zip │ ├── org.example.civixsnapshot-v17.10.5-kitchensink │ └── original.zip │ ├── org.example.civixsnapshot-v17.10.5-qf │ └── original.zip │ ├── org.example.civixsnapshot-v18.12.0-empty │ └── original.zip │ ├── org.example.civixsnapshot-v18.12.0-entity3 │ └── original.zip │ ├── org.example.civixsnapshot-v18.12.0-kitchensink │ └── original.zip │ ├── org.example.civixsnapshot-v18.12.0-qf │ └── original.zip │ ├── org.example.civixsnapshot-v19.11.0-empty │ └── original.zip │ ├── org.example.civixsnapshot-v19.11.0-entity3 │ └── original.zip │ ├── org.example.civixsnapshot-v19.11.0-kitchensink │ └── original.zip │ ├── org.example.civixsnapshot-v19.11.0-qf │ └── original.zip │ ├── org.example.civixsnapshot-v20.09.0-empty │ └── original.zip │ ├── org.example.civixsnapshot-v20.09.0-entity3 │ └── original.zip │ ├── org.example.civixsnapshot-v20.09.0-kitchensink │ └── original.zip │ ├── org.example.civixsnapshot-v20.09.0-qf │ └── original.zip │ ├── org.example.civixsnapshot-v22.02.0-empty │ └── original.zip │ ├── org.example.civixsnapshot-v22.02.0-entity3 │ └── original.zip │ ├── org.example.civixsnapshot-v22.02.0-entity34 │ └── original.zip │ ├── org.example.civixsnapshot-v22.02.0-kitchensink │ └── original.zip │ ├── org.example.civixsnapshot-v22.02.0-qf │ └── original.zip │ ├── org.example.civixsnapshot-v22.06.0-empty │ └── original.zip │ ├── org.example.civixsnapshot-v22.06.0-entity3 │ └── original.zip │ ├── org.example.civixsnapshot-v22.06.0-entity34 │ └── original.zip │ ├── org.example.civixsnapshot-v22.06.0-kitchensink │ └── original.zip │ ├── org.example.civixsnapshot-v22.06.0-qf │ └── original.zip │ ├── org.example.civixsnapshot-v22.10.2-empty │ └── original.zip │ ├── org.example.civixsnapshot-v22.10.2-entity3 │ └── original.zip │ ├── org.example.civixsnapshot-v22.10.2-entity34 │ └── original.zip │ ├── org.example.civixsnapshot-v22.10.2-kitchensink │ └── original.zip │ ├── org.example.civixsnapshot-v22.10.2-qf │ └── original.zip │ ├── org.example.civixsnapshot-v23.12.1-svc │ └── original.zip │ ├── org.example.civixsnapshot-v23.12.2-entity34 │ └── original.zip │ ├── org.example.civixsnapshot-v23.12.2-entity4 │ └── original.zip │ └── org.example.civixsnapshot-v24.09.1-entity4 │ └── original.zip └── upgrades ├── 16.10.0.up.php ├── 19.06.2.up.php ├── 20.06.0.up.php ├── 22.05.0.up.php ├── 22.05.2.up.php ├── 22.10.0.up.php ├── 22.12.1.up.php ├── 23.01.0.up.php ├── 23.02.0.up.php ├── 23.02.1.up.php ├── 24.09.0.up.php ├── 24.09.1.up.php ├── 25.01.0.up.php └── 25.01.1.up.php /.gitignore: -------------------------------------------------------------------------------- 1 | app/bootstrap.php.cache 2 | app/cache/* 3 | app/config/parameters.yml 4 | app/logs/* 5 | bin/civix.phar 6 | bin/cv 7 | bin/php-parse 8 | bin/psysh 9 | bin/var-dump-server 10 | build/ 11 | /tests/snapshots/*/upgrade 12 | /tests/snapshots/*/upgrade.log 13 | /tests/snapshots/*/upgrade.err 14 | /tests/snapshots/*/upgrade.diff 15 | /tests/snapshots/org.example.civixsnapshot-HEAD-* 16 | /extern/ 17 | /tmp/ 18 | vendor 19 | composer.phar 20 | *~ 21 | .idea 22 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ### v14.01.0 2 | 3 | * **generate:case-type** - Add command to generate CiviCase XML files 4 | * **generate:module** - Add support for generating license metadata by passing parameters "--license", "--author", and "--email". 5 | The information will be propagated to info.xml and LICENSE.txt 6 | * **generate:module** - Add hook stub for `hook_civicrm_alterSettingsFolders` (in module.php.php and module.civix.php.php) 7 | * **generate:module** - Add hook stub for `hook_civicrm_caseTypes` (in module.php.php and module.civix.php.php) 8 | * Add documentation links for hooks (using "@link") 9 | * Reformat civix source code based on CiviCRM's coding conventions 10 | 11 | ### v14.09.0 12 | 13 | * Add options **author**, **email**, **license** for setting defaults on new extensions 14 | * Remove options **civicrm_api3_conf_path**, **civicrm_api3_server**, **civicrm_api3_path**, **civicrm_api3_key**, and **civicrm_api3_api_key** 15 | * **civix** will scan the directory tree to locate and bootstrap the CMS. This requires that extensions be stored in a subdirectory somewhere under the CMS root. 16 | * **civix generate:module** - Automatically refresh extension list. Prompt for installation. 17 | * **civix generate:api** - Fix for v4.5 18 | 19 | ### v14.09.1 20 | 21 | * **civix generate:module** - Initialize URL tags in info.xml 22 | * **civix generate:entity** - Fix for v4.5 23 | * Fixes for misc PHPStorm warnings 24 | 25 | ## v15.04.0 26 | 27 | * Bootstrap - Update civicrm.settings.php search algorithm 28 | * Bootstrap - Fix for Drupal sites in subdirectories (below webroot) 29 | * **civix test** - Add options 30 | * Style cleanup on generated PHP (per Drupal phpcs) 31 | 32 | ## v16.02.0 33 | 34 | * Port from Symfony Standard Edition to thinner Symfony Components (Console) 35 | * Package as PHAR archive 36 | * Renamed `./civix` and `./civix.bat` to `./bin/civix`. For git-based installations, you may need to update the `PATH`. 37 | * Removed unused/incidental Symfony commands (e.g. `config:dump-reference` or `cache:warmup`) 38 | * **civix generate:report-ext** - Removed command. Specialized extensions (report/search/payment) have been deprecated for a long time 39 | * **civix test** - Deprecated. With [testapalooza](https://github.com/civicrm/org.civicrm.testapalooza), one can launch phpunit directly 40 | * **civix generate:module** - Add hook stub for `hook_civicrm_navigationMenu`. 41 | * Misc style/documentation tweaks in templates. 42 | 43 | ## v16.02.1 44 | 45 | * Embed `cv` bootstrap logic 46 | 47 | ## v16.03.0 48 | 49 | * Report better error when called from wrong directory 50 | 51 | ## v16.03.1 52 | 53 | * **civix generate:test** - Generate templates based on testapalooze (`phpunit.xml.dist`, `tests/phpunit/bootstrap.php`, `CiviTestListener`) 54 | 55 | ## v16.03.2 56 | 57 | * **civix generate:test** - Rename `--type` to `--template`. Add `legacy` template. More in-app help. 58 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # civix 2 | 3 | Civix is a command-line tool for building CiviCRM extensions. It is distributed as part of [CiviCRM-Buildkit](https://github.com/civicrm/civicrm-buildkit). 4 | 5 | ## Requirements 6 | 7 | * PHP 7.3+ 8 | * CiviCRM 5.x (*Recommended: any release from the prior 12 months*) 9 | * (For MAMP, WAMP, XAMPP, etc) PHP command-line configuration (http://wiki.civicrm.org/confluence/display/CRMDOC/Setup+Command-Line+PHP) 10 | * (For CentOS/RHEL) Compatible version of libxml2 (https://github.com/totten/civix/issues/19) 11 | 12 | ## Download 13 | 14 | `civix` is distributed in PHAR format, which is a portable executable file (for PHP). It should run on most Unix-like systems where PHP is installed. 15 | Here are three quick ways to download it: 16 | 17 | * Download [the latest release of `civix.phar`](https://download.civicrm.org/civix/civix.phar) (*[SHA256](https://download.civicrm.org/civix/civix.SHA256SUMS), 18 | [GPG](https://download.civicrm.org/civix/civix.phar.asc)*) and put it in the PATH. For example: 19 | 20 | ```bash 21 | sudo curl -LsS https://download.civicrm.org/civix/civix.phar -o /usr/local/bin/civix 22 | sudo chmod +x /usr/local/bin/civix 23 | ``` 24 | 25 | (*Learn more: [Install `civix.phar` as system-wide tool (Linux/BSD/macOS)](doc/download.md#phar-unix)*) 26 | 27 | * Or... add `civix` and other CiviCRM tools to a composer project (Drupal 9/10/11) 28 | 29 | ```bash 30 | composer require civicrm/cli-tools 31 | ``` 32 | 33 | (*Learn more: [Install `civix.phar` as project tool (composer)](doc/download.md#phar-composer)*) 34 | 35 | * Or... use [phar.io's `phive` installer](https://phar.io/) to download, validate, and cache the `civix.phar` file. 36 | 37 | ```bash 38 | phive install totten/civix 39 | ``` 40 | 41 | (*Learn more: [Install `civix.phar` as project tool (phive)](doc/download.md#phar-phive)*) 42 | 43 | There are several more options for downloading `civix`. See also: 44 | 45 | * [Download URLs for alternate versions](doc/download.md#urls) 46 | * [Comparison of install options](doc/download.md#comparison) 47 | * Install `civix` as a system-wide/standalone tool 48 | * [Install `civix.phar` (binary) as system-wide tool (Linux/BSD/macOS)](doc/download.md#phar-unix) 49 | * [Install `civix.git` (source) as standalone project (Linux/BSD/macOS)](doc/download.md#src-unix) 50 | * [Install `civix.git` (source) as standalone project (Windows)](doc/download.md#src-win) 51 | * Install `civix` as a tool within another project 52 | * [Install `civix.phar` (binary) as project tool (composer)](doc/download.md#phar-composer) 53 | * [Install `civix.phar` (binary) as project tool (phive)](doc/download.md#phar-phive) 54 | * [Install `civix.git` (source) as project tool (composer)](doc/download.md#src-composer) 55 | 56 | ## Documentation 57 | 58 | The [CiviCRM Developer Guide](https://docs.civicrm.org/dev/en/latest/) includes [tutorials for building extensions](https://docs.civicrm.org/dev/en/latest/extensions/civix/) 59 | 60 | For reference documentation, civix supports a "--help" option. For example, 61 | to get reference materials about the "generate:page" command, run: 62 | 63 | ```bash 64 | civix generate:page --help 65 | ``` 66 | 67 | ## Development 68 | 69 | If you are developing updates for `civix.git`, then see [doc/develop.md](doc/develop.md). It discusses PHAR compilation, unit tests, and similar processes. 70 | -------------------------------------------------------------------------------- /bin/civix: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env php 2 | =7.3.0", 8 | "civicrm/cv-lib": "~0.3.60", 9 | "civicrm/composer-compile-plugin": "~0.18", 10 | "civicrm/composer-downloads-plugin": "~2.1|^3", 11 | "civicrm/php-array-doc": "~0.1.7", 12 | "symfony/console": "^4|^5", 13 | "symfony/filesystem": "^4|^5", 14 | "symfony/process": "^4|^5", 15 | "symfony/templating": "^4|^5", 16 | "symfony/var-dumper": "^4|^5", 17 | "symfony/var-exporter": "^4.4|^5", 18 | "totten/license-data": "dev-master", 19 | "totten/process-helper": "^1.0.1", 20 | "webmozart/glob": "^4.7" 21 | }, 22 | "autoload": { 23 | "files": ["src/Civix.php"], 24 | "psr-0": { 25 | "Mixlib": "extern/src/", 26 | "CRM\\CivixBundle": "src/" 27 | } 28 | }, 29 | "bin": [ 30 | "bin/civix" 31 | ], 32 | "config": { 33 | "allow-plugins": { 34 | "civicrm/composer-compile-plugin": true, 35 | "civicrm/composer-downloads-plugin": true 36 | }, 37 | "platform": { 38 | "php": "7.3.0" 39 | }, 40 | "bin-dir": "bin" 41 | }, 42 | "authors": [ 43 | { 44 | "name": "Tim Otten", 45 | "email": "to-git@think.hm" 46 | } 47 | ], 48 | "extra": { 49 | "compile": [ 50 | { 51 | "title": "Download mixins (mixin-backports.php)", 52 | "run": "@php-method \\CRM\\CivixBundle\\ComposerCompile::downloadMixins" 53 | } 54 | ], 55 | "downloads": { 56 | "mixlib": { 57 | "version": "5.45", 58 | "url": "https://raw.githubusercontent.com/civicrm/civicrm-core/{$version}/tools/mixin/src/Mixlib.php", 59 | "path": "extern/src/Mixlib.php", 60 | "type": "file" 61 | } 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /lib/README.md: -------------------------------------------------------------------------------- 1 | # civix: Mixin Libraries 2 | 3 | ## Generate an update 4 | 5 | Consider `civimix-schema@X.X.X.phar`, which is generated from `civicrm-core:mixin/lib/civimix-schema`. 6 | 7 | Suppose a new version (`Y.Y.Y`) is available upstream. 8 | 9 | Here's how to update the version civix: 10 | 11 | ```bash 12 | ## Export new lib 13 | cd CIVICRM_REPO 14 | ./tools/mixin/bin/build-lib /tmp/civimix 15 | 16 | ## Update civix: Remove old lib. Add new lib. 17 | cd CIVIX_REPO 18 | rm lib/civimix-schema@X.X.X.phar 19 | cp /tmp/civimix/civimix-schema@Y.Y.Y.phar lib/ 20 | ``` 21 | -------------------------------------------------------------------------------- /lib/civimix-schema@5.85.beta1.phar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/totten/civix/b4c7e5fe832cb88a91b503a6f7ea436e7637f749/lib/civimix-schema@5.85.beta1.phar -------------------------------------------------------------------------------- /mixin-mods/README.md: -------------------------------------------------------------------------------- 1 | # mixin-mods 2 | 3 | This folder contains civix-specific forks of some mixins. Guidelines: 4 | 5 | * Always use a lower number than the current published. 6 | * Keep the semantics/behavior in close alignment with same-version upstream. 7 | * If you can't provide the same contract, then don't bother trying to backport. 8 | * Only make changes for backward compatibility (Civi APIs, PHP APIs). 9 | * To make a new variant, make a new copy. Keep old variants around for reference. 10 | * You still need to maintain `mixin-backports.php`. 11 | -------------------------------------------------------------------------------- /nix/buildkit-update.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | { # https://stackoverflow.com/a/21100710 3 | 4 | ## Re-generate the buildkit.nix file - with the current 'master' branch. 5 | 6 | set -e 7 | 8 | if [ ! -f "nix/buildkit.nix" ]; then 9 | echo >&2 "Must run in project root" 10 | exit 1 11 | fi 12 | 13 | now=$( date -u '+%Y-%m-%d %H:%M %Z' ) 14 | commit=$( git ls-remote https://github.com/civicrm/civicrm-buildkit.git | awk '/refs\/heads\/master$/ { print $1 }' ) 15 | url="https://github.com/civicrm/civicrm-buildkit/archive/${commit}.tar.gz" 16 | hash=$( nix-prefetch-url "$url" --type sha256 --unpack ) 17 | 18 | function render_file() { 19 | echo "{ pkgs ? import {} }:" 20 | echo "" 21 | echo "## Get civicrm-buildkit from github." 22 | echo "## Based on \"master\" branch circa $now" 23 | echo "import (pkgs.fetchzip {" 24 | echo " url = \"$url\";" 25 | echo " sha256 = \"$hash\";" 26 | echo "})" 27 | echo 28 | echo "## Get a local copy of civicrm-buildkit. (Useful for developing patches.)" 29 | echo "# import ((builtins.getEnv \"HOME\") + \"/buildkit/default.nix\")" 30 | echo "# import ((builtins.getEnv \"HOME\") + \"/bknix/default.nix\")" 31 | } 32 | render_file > nix/buildkit.nix 33 | 34 | exit 35 | } 36 | -------------------------------------------------------------------------------- /nix/buildkit.nix: -------------------------------------------------------------------------------- 1 | { pkgs ? import {} }: 2 | 3 | ## Get civicrm-buildkit from github. 4 | ## Based on "master" branch circa 2024-02-26 04:31 UTC 5 | import (pkgs.fetchzip { 6 | url = "https://github.com/civicrm/civicrm-buildkit/archive/d6f6b8dd2d5944c35cd78cb319fef21673214b35.tar.gz"; 7 | sha256 = "02p2yzdfgv66a2zf8i36h6pjfi78wnp92m3klij7fqbfd9mpvi5a"; 8 | }) 9 | 10 | ## Get a local copy of civicrm-buildkit. (Useful for developing patches.) 11 | # import ((builtins.getEnv "HOME") + "/buildkit/default.nix") 12 | # import ((builtins.getEnv "HOME") + "/bknix/default.nix") 13 | -------------------------------------------------------------------------------- /phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 15 | 16 | 17 | ./src 18 | 19 | 20 | src/*Bundle/Resources 21 | src/*/*Bundle/Resources 22 | src/*/Bundle/*Bundle/Resources 23 | 24 | 25 | 26 | 27 | ./src/ 28 | 29 | 30 | ./tests/e2e/ 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /scoper.inc.php: -------------------------------------------------------------------------------- 1 | 'CivixPhar', 5 | 6 | 'exclude-namespaces' => [ 7 | // Provided by civicrm 8 | 'Civi', 9 | 'Guzzle', 10 | 'Symfony\Component\DependencyInjection', 11 | 12 | // Drupal8+ bootstrap 13 | 'Drupal', 14 | 'Symfony\\Component\\HttpFoundation', 15 | 'Symfony\\Component\\Routing', 16 | 17 | // Joomla bootstrap 18 | 'TYPO3\\PharStreamWrapper', 19 | ], 20 | 21 | 'exclude-classes' => [ 22 | '/^(CRM_|HTML_|DB_|Log_)/', 23 | 'civicrm_api3', 24 | 'Mixlib', 25 | 'DB', 26 | 'Log', 27 | 'JFactory', 28 | 'Civi', 29 | 'Drupal', 30 | ], 31 | 'exclude-functions' => [ 32 | '/^civicrm_/', 33 | '/_civicrm_api_get_entity_name_from_camel/', 34 | '/^wp_.*/', 35 | '/^(drupal|backdrop|user|module)_/', 36 | 't', 37 | ], 38 | 39 | // Do not generate wrappers/aliases for `civicrm_api()` etc or various CMS-booting functions. 40 | 'expose-global-functions' => FALSE, 41 | 42 | // Do not filter template files 43 | 'exclude-files' => array_merge( 44 | glob('lib/*.phar'), 45 | glob('lib/*.php'), 46 | glob('src/CRM/CivixBundle/Resources/views/*/*.php'), 47 | glob('extern/*/*/*.php'), 48 | glob('extern/*/*.php'), 49 | glob('vendor/symfony/polyfill-php80/Resources/stubs/*php'), /* polyfill-php80@1.27.0 + box@4.8.3 */ 50 | ), 51 | 52 | ]; 53 | -------------------------------------------------------------------------------- /scripts/build.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | ## Determine the absolute path of the directory with the file 4 | ## usage: absdirname 5 | function absdirname() { 6 | pushd $(dirname $0) >> /dev/null 7 | pwd 8 | popd >> /dev/null 9 | } 10 | 11 | SCRDIR=$(absdirname "$0") 12 | PRJDIR=$(dirname "$SCRDIR") 13 | OUTFILE="$PRJDIR/bin/civix.phar" 14 | set -ex 15 | 16 | ## Box's temp file convention is not multi-user aware. Prone to permission error when second user tries to write. 17 | export TMPDIR="/tmp/box-$USER" 18 | if [ ! -d "$TMPDIR" ]; then mkdir "$TMPDIR" ; fi 19 | 20 | pushd "$PRJDIR" >> /dev/null 21 | composer install --prefer-dist --no-progress --no-suggest --no-dev 22 | nix-shell --run 'box compile -v' 23 | php scripts/check-phar.php "$OUTFILE" 24 | popd >> /dev/null 25 | -------------------------------------------------------------------------------- /scripts/run-tests.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | ## Run PHPUnit tests. This is a small wrapper for `phpunit` which does some setup for the E2E enviroment. 5 | ## 6 | ## You may optionally specify whether to run E2E tests using the raw civix source or compiled civix.phar. 7 | ## 8 | ## usage: ./scripts/run-tests.sh [--civix-src|--civix-phar] [phpunit-args...] 9 | 10 | ################################################ 11 | CIVIX_BUILD_TYPE=src 12 | case "$1" in 13 | --civix-src|--civix-phar) CIVIX_BUILD_TYPE="$1" ; shift ; ;; 14 | esac 15 | 16 | ################################################ 17 | ## Didn't set a workspace? Educated guess... 18 | if [ -z "$CIVIX_WORKSPACE" -a -d "$CIVIBUILD_HOME/dmaster/web/sites/all/modules/civicrm" ]; then 19 | export CIVIX_WORKSPACE="$CIVIBUILD_HOME/dmaster/web/sites/all/modules/civicrm/ext/civixtest" 20 | echo "Inferred CIVIX_WORKSPACE=$CIVIX_WORKSPACE" 21 | fi 22 | if [ -z "$CIVIX_WORKSPACE" ]; then 23 | echo "Missing env var: CIVIX_WORKSPACE" 24 | exit 1 25 | fi 26 | 27 | ################################################ 28 | if [ "$CIVIX_BUILD_TYPE" = "--civix-phar" ]; then 29 | if [ ! -f "box.json" -o ! -f "scripts/build.sh" ]; then 30 | echo "Must call from civix root dir" 31 | exit 1 32 | fi 33 | ./scripts/build.sh 34 | CIVIX_TEST_BINARY="$PWD"/bin/civix.phar 35 | export CIVIX_TEST_BINARY 36 | else 37 | unset CIVIX_TEST_BINARY 38 | fi 39 | 40 | ################################################ 41 | [ ! -d "$CIVIX_WORKSPACE" ] && mkdir -p "$CIVIX_WORKSPACE" || echo '' 42 | (cd "$CIVIX_WORKSPACE" && XDEBUG_MODE=off civibuild restore) 43 | 44 | phpunit9 "$@" 45 | -------------------------------------------------------------------------------- /scripts/tidy-snapshots.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | ## Delete ephemeral data like `tests/snapshots/*/upgrade` 4 | 5 | ################################################ 6 | EXMODULE=${EXMODULE:-org.example.civixsnapshot} 7 | SNAPSHOT_DIR="$PWD/tests/snapshots" 8 | 9 | ################################################ 10 | 11 | rm -rf "$SNAPSHOT_DIR"/*/upgrade 12 | rm -f "$SNAPSHOT_DIR"/*/upgrade.diff 13 | rm -f "$SNAPSHOT_DIR"/*/upgrade.log 14 | rm -rf "$SNAPSHOT_DIR"/org.example.civixsnapshot-HEAD-* 15 | -------------------------------------------------------------------------------- /scripts/upgrade-snapshots.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | ## Do a trial run -- by upgrading some of the snapshots. 4 | ## This is a wrapper for PHPUnit that twiddles some of the environment. 5 | 6 | if [ -z "$1" ]; then 7 | echo "Try out the upgrade procedure on a set of snapshots" 8 | echo "" 9 | echo "usage: $0 " 10 | echo "example: $0 '/v16/'" 11 | echo "example: $0 '/-qf/'" 12 | echo "example: $0 '/v22.*-entity/'" 13 | echo 14 | echo "Snapshots are read from 'tests/snapshots/*/original.zip'" 15 | echo "Upgrades (with logs and diffs) are written to 'tests/snapshots/*/upgrade'" 16 | exit 1 17 | fi 18 | 19 | echo "Snapshots are read from: $PWD/tests/snapshots/*/original.zip" 20 | echo "Upgrades will be written to: $PWD/tests/snapshots/*/upgrade" 21 | 22 | ################################################ 23 | ## Didn't set a workspace? Educated guess... 24 | if [ -z "$CIVIX_WORKSPACE" -a -d "$CIVIBUILD_HOME/dmaster/web/sites/all/modules/civicrm" ]; then 25 | export CIVIX_WORKSPACE="$CIVIBUILD_HOME/dmaster/web/sites/all/modules/civicrm/ext/civixtest" 26 | echo "Inferred CIVIX_WORKSPACE=$CIVIX_WORKSPACE" 27 | fi 28 | if [ -z "$CIVIX_WORKSPACE" ]; then 29 | echo "Missing env var: CIVIX_WORKSPACE" 30 | exit 1 31 | fi 32 | 33 | ################################################ 34 | export SNAPSHOT_SAVE=1 35 | export SNAPSHOT_FILTER="$1" 36 | phpunit9 tests/e2e/SnapshotUpgradeTest.php --debug 37 | -------------------------------------------------------------------------------- /shell.nix: -------------------------------------------------------------------------------- 1 | /** 2 | * This shell is suitable for compiling PHAR executables.... and not much else. 3 | * 4 | * Ex: `nix-shell --run ./scripts/build.sh` 5 | */ 6 | 7 | { pkgs ? import {} }: 8 | 9 | let 10 | 11 | buildkit = (import ./nix/buildkit.nix) { inherit pkgs; }; 12 | 13 | in 14 | 15 | pkgs.mkShell { 16 | nativeBuildInputs = buildkit.profiles.base ++ [ 17 | 18 | (buildkit.pins.v2305.php82.buildEnv { 19 | extraConfig = '' 20 | memory_limit=-1 21 | ''; 22 | }) 23 | 24 | buildkit.pkgs.box 25 | buildkit.pkgs.composer 26 | buildkit.pkgs.pogo 27 | buildkit.pkgs.phpunit8 28 | buildkit.pkgs.phpunit9 29 | 30 | pkgs.bash-completion 31 | ]; 32 | shellHook = '' 33 | source ${pkgs.bash-completion}/etc/profile.d/bash_completion.sh 34 | ''; 35 | } 36 | -------------------------------------------------------------------------------- /src/CRM/CivixBundle/Application.php: -------------------------------------------------------------------------------- 1 | run(); 12 | } 13 | 14 | public function __construct($name = 'UNKNOWN', $version = 'UNKNOWN') { 15 | parent::__construct($name, $version); 16 | $this->setCatchExceptions(TRUE); 17 | $this->addCommands($this->createCommands()); 18 | } 19 | 20 | /** 21 | * Construct command objects 22 | * 23 | * @return array of Symfony Command objects 24 | */ 25 | public function createCommands($context = 'default') { 26 | $commands = []; 27 | $commands[] = new Command\AddAngularDirectiveCommand(); 28 | $commands[] = new Command\AddAngularModuleCommand(); 29 | $commands[] = new Command\AddAngularPageCommand(); 30 | $commands[] = new Command\AddApiCommand(); 31 | $commands[] = new Command\AddCaseTypeCommand(); 32 | $commands[] = new Command\AddCustomDataCommand(); 33 | $commands[] = new Command\AddEntityCommand(); 34 | $commands[] = new Command\AddFormCommand(); 35 | $commands[] = new Command\AddManagedEntityCommand(); 36 | $commands[] = new Command\AddPageCommand(); 37 | $commands[] = new Command\AddReportCommand(); 38 | $commands[] = new Command\AddSearchCommand(); 39 | $commands[] = new Command\AddServiceCommand(); 40 | $commands[] = new Command\AddTestCommand(); 41 | $commands[] = new Command\AddThemeCommand(); 42 | $commands[] = new Command\AddUpgraderCommand(); 43 | $commands[] = new Command\BuildCommand(); 44 | $commands[] = new Command\ConfigGetCommand(); 45 | $commands[] = new Command\ConfigSetCommand(); 46 | $commands[] = new Command\InitCommand(); 47 | $commands[] = new Command\MixinCommand(); 48 | $commands[] = new Command\ConvertEntityCommand(); 49 | $commands[] = new Command\PingCommand(); 50 | $commands[] = new Command\TestRunCommand(); 51 | $commands[] = new Command\UpgradeCommand(); 52 | $commands[] = new Command\InfoGetCommand(); 53 | $commands[] = new Command\InfoSetCommand(); 54 | $commands[] = new Command\InspectFunctionCommand(); 55 | return $commands; 56 | } 57 | 58 | /** 59 | * Find the base path of the current extension 60 | * 61 | * @return string 62 | * Ex: "/var/www/extension/org.example.foobar". 63 | * @deprecated 64 | * @see \Civix::extDir() 65 | */ 66 | public static function findExtDir(): string { 67 | return (string) \Civix::extDir(); 68 | } 69 | 70 | /** 71 | * @return string 72 | * @deprecated 73 | */ 74 | public static function findCivixDir(): string { 75 | return (string) \Civix::appDir(); 76 | } 77 | 78 | } 79 | -------------------------------------------------------------------------------- /src/CRM/CivixBundle/Builder.php: -------------------------------------------------------------------------------- 1 | builders = $builders; 19 | } 20 | 21 | public function loadInit(&$ctx) { 22 | foreach ($this->builders as $builder) { 23 | $builder->loadInit($ctx); 24 | } 25 | } 26 | 27 | public function init(&$ctx) { 28 | foreach ($this->builders as $builder) { 29 | $builder->init($ctx); 30 | } 31 | } 32 | 33 | public function load(&$ctx) { 34 | print_r($this); 35 | foreach ($this->builders as $builder) { 36 | $builder->load($ctx); 37 | } 38 | } 39 | 40 | public function save(&$ctx, OutputInterface $output) { 41 | foreach ($this->builders as $builder) { 42 | $builder->save($ctx, $output); 43 | } 44 | } 45 | 46 | } 47 | -------------------------------------------------------------------------------- /src/CRM/CivixBundle/Builder/Content.php: -------------------------------------------------------------------------------- 1 | content = $content; 25 | $this->path = $path; 26 | $this->overwrite = $overwrite; 27 | } 28 | 29 | public function loadInit(&$ctx) { 30 | } 31 | 32 | public function init(&$ctx) { 33 | } 34 | 35 | public function load(&$ctx) { 36 | } 37 | 38 | /** 39 | * Write the content 40 | */ 41 | public function save(&$ctx, OutputInterface $output) { 42 | $parent = dirname($this->path); 43 | if (!is_dir($parent)) { 44 | mkdir($parent, Dirs::MODE, TRUE); 45 | } 46 | if (file_exists($this->path) && $this->overwrite === 'ignore') { 47 | // do nothing 48 | } 49 | elseif (file_exists($this->path) && !$this->overwrite) { 50 | $output->writeln("Skip " . Files::relativize($this->path) . ": file already exists"); 51 | } 52 | else { 53 | $output->writeln("Write " . Files::relativize($this->path)); 54 | file_put_contents($this->path, $this->getContent($ctx)); 55 | } 56 | } 57 | 58 | protected function getContent($ctx): string { 59 | return $this->content; 60 | } 61 | 62 | } 63 | -------------------------------------------------------------------------------- /src/CRM/CivixBundle/Builder/CopyClass.php: -------------------------------------------------------------------------------- 1 | srcClassName = $srcClassName; 29 | $this->tgtClassName = $tgtClassName; 30 | $this->tgtFile = $tgtFile; 31 | $this->overwrite = $overwrite; 32 | $this->enable = FALSE; 33 | $this->filter = $filter; 34 | } 35 | 36 | public function loadInit(&$ctx) { 37 | } 38 | 39 | public function init(&$ctx) { 40 | } 41 | 42 | public function load(&$ctx) { 43 | } 44 | 45 | /** 46 | * Write the xml document 47 | */ 48 | public function save(&$ctx, OutputInterface $output) { 49 | // NOTE: assume classloaders, etal, are already setup 50 | $clazz = new \ReflectionClass($this->srcClassName); 51 | if (file_exists($this->tgtFile) && $this->overwrite == 'ignore') { 52 | // do nothing 53 | } 54 | elseif (file_exists($this->tgtFile) && !$this->overwrite) { 55 | $output->writeln("Skip " . Files::relativize($this->tgtFile) . ": file already exists"); 56 | } 57 | else { 58 | $output->writeln("Write " . Files::relativize($this->tgtFile)); 59 | $content = file_get_contents($clazz->getFileName(), TRUE); 60 | // FIXME parser 61 | $content = strtr($content, [ 62 | $this->srcClassName => $this->tgtClassName, 63 | ]); 64 | if (is_callable($this->filter)) { 65 | $content = call_user_func($this->filter, $content); 66 | } 67 | file_put_contents($this->tgtFile, $content); 68 | } 69 | } 70 | 71 | } 72 | -------------------------------------------------------------------------------- /src/CRM/CivixBundle/Builder/CopyFile.php: -------------------------------------------------------------------------------- 1 | from = $from; 23 | $this->to = $to; 24 | $this->overwrite = $overwrite; 25 | $this->enable = FALSE; 26 | } 27 | 28 | public function loadInit(&$ctx) { 29 | } 30 | 31 | public function init(&$ctx) { 32 | } 33 | 34 | public function load(&$ctx) { 35 | } 36 | 37 | /** 38 | * Write the xml document 39 | */ 40 | public function save(&$ctx, OutputInterface $output) { 41 | if (file_exists($this->to) && $this->overwrite == 'ignore') { 42 | // do nothing 43 | } 44 | elseif (file_exists($this->to) && !$this->overwrite) { 45 | $output->writeln("Skip " . Files::relativize($this->to) . ": file already exists"); 46 | } 47 | else { 48 | $output->writeln("Write " . Files::relativize($this->to)); 49 | $content = file_get_contents($this->from, TRUE); 50 | file_put_contents($this->to, $content); 51 | } 52 | } 53 | 54 | } 55 | -------------------------------------------------------------------------------- /src/CRM/CivixBundle/Builder/CustomDataXML.php: -------------------------------------------------------------------------------- 1 | customGroupIds = $customGroupIds; 25 | $this->ufGroupIds = $ufGroupIds; 26 | $this->path = $path; 27 | $this->overwrite = $overwrite; 28 | $this->export = new \CRM_Utils_Migrate_Export(); 29 | $this->export->buildCustomGroups($this->customGroupIds); 30 | $this->export->buildUFGroups($this->ufGroupIds); 31 | } 32 | 33 | public function loadInit(&$ctx) { 34 | } 35 | 36 | public function init(&$ctx) { 37 | } 38 | 39 | public function load(&$ctx) { 40 | } 41 | 42 | /** 43 | * Write the xml document 44 | */ 45 | public function save(&$ctx, OutputInterface $output) { 46 | if (file_exists($this->path) && $this->overwrite === 'ignore') { 47 | // do nothing 48 | } 49 | elseif (file_exists($this->path) && !$this->overwrite) { 50 | $output->writeln("Skip " . Files::relativize($this->path) . ": file already exists"); 51 | } 52 | else { 53 | $output->writeln("Write " . Files::relativize($this->path)); 54 | file_put_contents($this->path, $this->export->toXML()); 55 | } 56 | } 57 | 58 | } 59 | -------------------------------------------------------------------------------- /src/CRM/CivixBundle/Builder/Dirs.php: -------------------------------------------------------------------------------- 1 | paths = $paths; 22 | } 23 | 24 | public function addPath(string $path) { 25 | $this->paths[] = $path; 26 | } 27 | 28 | public function loadInit(&$ctx) { 29 | } 30 | 31 | public function init(&$ctx) { 32 | } 33 | 34 | public function load(&$ctx) { 35 | } 36 | 37 | public function save(&$ctx, OutputInterface $output) { 38 | sort($this->paths); 39 | foreach ($this->paths as $dir) { 40 | $parts = explode(DIRECTORY_SEPARATOR, $dir); 41 | if (!is_dir($dir)) { 42 | //quiet//$output->writeln("Create ${dir}/"); 43 | mkdir($dir, self::MODE, TRUE); 44 | } 45 | else { 46 | // $output->writeln("Found ${dir}/"); 47 | } 48 | } 49 | } 50 | 51 | } 52 | -------------------------------------------------------------------------------- /src/CRM/CivixBundle/Builder/Ini.php: -------------------------------------------------------------------------------- 1 | path = $path; 30 | $this->header = $header; 31 | } 32 | 33 | /** 34 | * Get the xml document 35 | * 36 | * @return array 37 | */ 38 | public function get() { 39 | return $this->data; 40 | } 41 | 42 | public function set($data) { 43 | $this->data = $data; 44 | } 45 | 46 | public function loadInit(&$ctx) { 47 | if (file_exists($this->path)) { 48 | $this->load($ctx); 49 | } 50 | else { 51 | $this->init($ctx); 52 | } 53 | } 54 | 55 | /** 56 | * Initialize a new var_export() document 57 | */ 58 | public function init(&$ctx) { 59 | } 60 | 61 | /** 62 | * Read from file 63 | */ 64 | public function load(&$ctx) { 65 | $this->data = parse_ini_file($this->path, TRUE); 66 | } 67 | 68 | /** 69 | * Write the xml document 70 | */ 71 | public function save(&$ctx, OutputInterface $output) { 72 | $output->writeln("Write " . Files::relativize($this->path)); 73 | 74 | $content = ''; 75 | foreach ($this->data as $topKey => $data) { 76 | $content .= "[$topKey]\n"; 77 | foreach ($data as $key => $value) { 78 | if (is_array($value)) { 79 | $content .= $this->flatten($key, $value); 80 | } 81 | else { 82 | $content .= "$key=$value\n"; 83 | } 84 | } 85 | } 86 | file_put_contents($this->path, $content); 87 | } 88 | 89 | protected function flatten($prefix, $values) { 90 | $content = ''; 91 | foreach ($values as $key => $value) { 92 | if (is_array($values)) { 93 | $content .= $this->flatten("$prefix[$key]", $value); 94 | } 95 | else { 96 | $content .= "$prefix[$key]=$value\n"; 97 | } 98 | } 99 | return $content; 100 | } 101 | 102 | } 103 | -------------------------------------------------------------------------------- /src/CRM/CivixBundle/Builder/License.php: -------------------------------------------------------------------------------- 1 | license = $license; 39 | $this->path = $path; 40 | $this->overwrite = $overwrite; 41 | } 42 | 43 | public function loadInit(&$ctx) { 44 | } 45 | 46 | public function init(&$ctx) { 47 | } 48 | 49 | public function load(&$ctx) { 50 | } 51 | 52 | /** 53 | * Write the xml document 54 | */ 55 | public function save(&$ctx, OutputInterface $output) { 56 | if (file_exists($this->path) && $this->overwrite === 'ignore') { 57 | // do nothing 58 | } 59 | elseif (file_exists($this->path) && !$this->overwrite) { 60 | $output->writeln("Skip " . Files::relativize($this->path) . ": file already exists"); 61 | } 62 | else { 63 | $output->writeln("Write " . Files::relativize($this->path)); 64 | $text = strtr($this->license->getText(), [ 65 | '' => date('Y'), 66 | '' => sprintf('%s <%s>', $ctx['author'], $ctx['email']), 67 | '' => sprintf('Package: %s', $ctx['fullName']), 68 | ]); 69 | file_put_contents($this->path, $text); 70 | } 71 | } 72 | 73 | } 74 | -------------------------------------------------------------------------------- /src/CRM/CivixBundle/Builder/Menu.php: -------------------------------------------------------------------------------- 1 | <?php 2 | namespace CRM\CivixBundle\Builder; 3 | 4 | use SimpleXMLElement; 5 | use CRM\CivixBundle\Builder\XML; 6 | 7 | /** 8 | * Build/update menu xml file 9 | */ 10 | class Menu extends XML { 11 | 12 | public function init(&$ctx) { 13 | $xml = new SimpleXMLElement('<menu></menu>'); 14 | $this->set($xml); 15 | } 16 | 17 | public function hasPath($path) { 18 | $items = $this->get()->xpath(sprintf('item[path="%s"]', $path)); 19 | return !empty($items); 20 | } 21 | 22 | public function addItem($ctx, $title, $fullClass, $path) { 23 | $item = $this->get()->addChild('item'); 24 | $item->addChild('path', $path); 25 | $item->addChild('page_callback', $fullClass); 26 | $item->addChild('title', $title); 27 | $item->addChild('access_arguments', 'access CiviCRM'); 28 | } 29 | 30 | } 31 | -------------------------------------------------------------------------------- /src/CRM/CivixBundle/Builder/Module.php: -------------------------------------------------------------------------------- 1 | <?php 2 | namespace CRM\CivixBundle\Builder; 3 | 4 | use CRM\CivixBundle\Utils\MixinLibraries; 5 | use Symfony\Component\Console\Output\OutputInterface; 6 | use CRM\CivixBundle\Builder; 7 | use CRM\CivixBundle\Utils\Path; 8 | 9 | class Module implements Builder { 10 | 11 | /** 12 | * @var \Symfony\Component\Templating\EngineInterface 13 | */ 14 | public $templateEngine; 15 | 16 | public function __construct($templateEngine) { 17 | $this->templateEngine = $templateEngine; 18 | } 19 | 20 | public function loadInit(&$ctx) { 21 | } 22 | 23 | public function init(&$ctx) { 24 | } 25 | 26 | public function load(&$ctx) { 27 | } 28 | 29 | public function save(&$ctx, OutputInterface $output) { 30 | $basedir = new Path($ctx['basedir']); 31 | 32 | \Civix::generator()->updateMixinLibraries(function(MixinLibraries $libs) { 33 | $useSchema = \Civix::checker()->hasUpgrader() 34 | && !\Civix::checker()->coreProvidesLibrary('civimix-schema@5'); 35 | $libs->toggle('civimix-schema@5', $useSchema); 36 | }); 37 | 38 | $module = new Template( 39 | 'module.php.php', 40 | $basedir->string($ctx['mainFile'] . '.php'), 41 | 'ignore', 42 | $this->templateEngine 43 | ); 44 | $module->save($ctx, $output); 45 | 46 | $moduleCivix = new Template( 47 | 'module.civix.php.php', 48 | $basedir->string($ctx['mainFile'] . '.civix.php'), 49 | TRUE, 50 | $this->templateEngine 51 | ); 52 | 53 | $moduleCivix->save($ctx, $output); 54 | } 55 | 56 | } 57 | -------------------------------------------------------------------------------- /src/CRM/CivixBundle/Builder/PHPUnitGenerateInitFiles.php: -------------------------------------------------------------------------------- 1 | <?php 2 | namespace CRM\CivixBundle\Builder; 3 | 4 | use Civix; 5 | use CRM\CivixBundle\Utils\Files; 6 | use Symfony\Component\Console\Output\OutputInterface; 7 | 8 | class PHPUnitGenerateInitFiles { 9 | 10 | /** 11 | * @param $bootstrapFile 12 | * @param $ctx 13 | * @param \Symfony\Component\Console\Output\OutputInterface $output 14 | * @param $renderTemplateName 15 | */ 16 | public function initPhpunitBootstrap($bootstrapFile, &$ctx, OutputInterface $output) { 17 | if (!file_exists($bootstrapFile)) { 18 | $dirs = new Dirs([ 19 | dirname($bootstrapFile), 20 | ]); 21 | $dirs->save($ctx, $output); 22 | 23 | $output->writeln(sprintf('<info>Write</info> %s', Files::relativize($bootstrapFile))); 24 | file_put_contents($bootstrapFile, Civix::templating() 25 | ->render('phpunit-boot-cv.php.php', $ctx)); 26 | } 27 | else { 28 | $output->writeln(sprintf('<comment>Skip %s: file already exists</comment>', Files::relativize($bootstrapFile))); 29 | } 30 | } 31 | 32 | /** 33 | * @param $phpunitXmlFile 34 | * @param $ctx 35 | * @param \Symfony\Component\Console\Output\OutputInterface $output 36 | */ 37 | public function initPhpunitXml($phpunitXmlFile, &$ctx, OutputInterface $output) { 38 | if (!file_exists($phpunitXmlFile)) { 39 | $phpunitXml = new PhpUnitXML($phpunitXmlFile); 40 | $phpunitXml->init($ctx); 41 | $phpunitXml->save($ctx, $output); 42 | } 43 | else { 44 | $output->writeln(sprintf('<comment>Skip %s: file already exists</comment>', Files::relativize($phpunitXmlFile))); 45 | } 46 | } 47 | 48 | } 49 | -------------------------------------------------------------------------------- /src/CRM/CivixBundle/Builder/PhpUnitXML.php: -------------------------------------------------------------------------------- 1 | <?php 2 | namespace CRM\CivixBundle\Builder; 3 | 4 | use SimpleXMLElement; 5 | 6 | /** 7 | * Build phpunit.xml.dist 8 | */ 9 | class PhpUnitXML extends XML { 10 | 11 | public function init(&$ctx) { 12 | $xml = new SimpleXMLElement('<phpunit></phpunit>'); 13 | $xml->addAttribute('backupGlobals', 'false'); 14 | $xml->addAttribute('backupStaticAttributes', 'false'); 15 | $xml->addAttribute('colors', 'true'); 16 | $xml->addAttribute('convertErrorsToExceptions', 'true'); 17 | $xml->addAttribute('convertNoticesToExceptions', 'true'); 18 | $xml->addAttribute('convertWarningsToExceptions', 'true'); 19 | $xml->addAttribute('convertDeprecationsToExceptions', 'true'); 20 | $xml->addAttribute('processIsolation', 'false'); 21 | $xml->addAttribute('stopOnFailure', 'false'); 22 | $xml->addAttribute('cacheResult', 'false'); 23 | $xml->addAttribute('bootstrap', 'tests/phpunit/bootstrap.php'); 24 | 25 | $xml->registerXPathNamespace('xsi', 'http://www.w3.org/2001/XMLSchema-instance'); 26 | $xml->addAttribute('xsi:noNamespaceSchemaLocation', 'https://schema.phpunit.de/9.3/phpunit.xsd', 'http://www.w3.org/2001/XMLSchema-instance'); 27 | 28 | $this->set($xml); 29 | 30 | $this->addTestSuite($ctx['fullName'] . ' Tests', ['./tests/phpunit']); 31 | 32 | $this->get() 33 | ->addChild('coverage') 34 | ->addChild('include') 35 | ->addChild('directory', './') 36 | ->addAttribute('suffix', '.php'); 37 | 38 | $listenerXml = $this->get()->addChild('listeners')->addChild('listener'); 39 | $listenerXml->addAttribute('class', 'Civi\\Test\\CiviTestListener'); 40 | $listenerXml->addChild('arguments', ''); 41 | } 42 | 43 | /** 44 | * @param string $name 45 | * @param array $dirs 46 | */ 47 | public function addTestSuite($name, $dirs) { 48 | $testsuites = $this->get()->addChild('testsuites'); /* FIXME: find/load */ 49 | $testsuite = $testsuites->addChild('testsuite'); 50 | $testsuite->addAttribute('name', $name); 51 | foreach ($dirs as $dir) { 52 | $testsuite->addChild('directory', $dir); 53 | } 54 | } 55 | 56 | } 57 | -------------------------------------------------------------------------------- /src/CRM/CivixBundle/Builder/Template.php: -------------------------------------------------------------------------------- 1 | <?php 2 | namespace CRM\CivixBundle\Builder; 3 | 4 | use Civix; 5 | 6 | /** 7 | * Build/update a file based on a template 8 | */ 9 | class Template extends Content { 10 | 11 | protected $template; 12 | protected $xml; 13 | protected $templateEngine; 14 | 15 | /** 16 | * @param string $template 17 | * @param string $path 18 | * @param bool|string $overwrite TRUE (always overwrite), FALSE (preserve with error), 'ignore' (preserve quietly) 19 | * @param \Symfony\Component\Templating\EngineInterface|null $templateEngine 20 | * @param 21 | */ 22 | public function __construct(string $template, string $path, $overwrite = FALSE, $templateEngine = NULL) { 23 | $this->template = $template; 24 | $this->path = $path; 25 | $this->overwrite = $overwrite; 26 | $this->templateEngine = $templateEngine ?: Civix::templating(); 27 | } 28 | 29 | protected function getContent($ctx): string { 30 | return $this->templateEngine->render($this->template, $ctx); 31 | } 32 | 33 | } 34 | -------------------------------------------------------------------------------- /src/CRM/CivixBundle/Builder/XML.php: -------------------------------------------------------------------------------- 1 | <?php 2 | namespace CRM\CivixBundle\Builder; 3 | 4 | use CRM\CivixBundle\Utils\Files; 5 | use SimpleXMLElement; 6 | use DOMDocument; 7 | use CRM\CivixBundle\Builder; 8 | use Symfony\Component\Console\Output\OutputInterface; 9 | 10 | /** 11 | * Build/update an XML file 12 | */ 13 | class XML implements Builder { 14 | 15 | protected $path, $xml; 16 | 17 | public function __construct($path) { 18 | $this->path = $path; 19 | } 20 | 21 | /** 22 | * Get the xml document 23 | * 24 | * @return SimpleXMLElement 25 | */ 26 | public function get() { 27 | return $this->xml; 28 | } 29 | 30 | public function set($xml) { 31 | $this->xml = $xml; 32 | } 33 | 34 | public function loadInit(&$ctx) { 35 | if (file_exists($this->path)) { 36 | $this->load($ctx); 37 | } 38 | else { 39 | $this->init($ctx); 40 | } 41 | } 42 | 43 | /** 44 | * Initialize a new XML document 45 | */ 46 | public function init(&$ctx) { 47 | } 48 | 49 | /** 50 | * Read from file 51 | */ 52 | public function load(&$ctx) { 53 | $dom = new DomDocument(); 54 | $dom->load($this->path); 55 | $dom->xinclude(); 56 | $this->xml = simplexml_import_dom($dom); 57 | } 58 | 59 | /** 60 | * Write the xml document 61 | */ 62 | public function save(&$ctx, OutputInterface $output) { 63 | $output->writeln("<info>Write</info> " . Files::relativize($this->path)); 64 | 65 | // force pretty printing with encode/decode cycle 66 | $outXML = $this->get()->saveXML(); 67 | $xml = new DOMDocument(); 68 | $xml->encoding = 'iso-8859-1'; 69 | $xml->preserveWhiteSpace = FALSE; 70 | $xml->formatOutput = TRUE; 71 | $xml->loadXML($outXML); 72 | file_put_contents($this->path, $xml->saveXML()); 73 | } 74 | 75 | } 76 | -------------------------------------------------------------------------------- /src/CRM/CivixBundle/CivixTestListener.php: -------------------------------------------------------------------------------- 1 | <?php 2 | namespace CRM\CivixBundle; 3 | 4 | use PHPUnit\Framework\Test; 5 | 6 | class CivixTestListener implements \PHPUnit\Framework\TestListener { 7 | use \PHPUnit\Framework\TestListenerDefaultImplementation; 8 | 9 | public function startTest(Test $test): void { 10 | \Civix::reset(); 11 | } 12 | 13 | } 14 | -------------------------------------------------------------------------------- /src/CRM/CivixBundle/Command/AbstractCommand.php: -------------------------------------------------------------------------------- 1 | <?php 2 | namespace CRM\CivixBundle\Command; 3 | 4 | use CRM\CivixBundle\Builder\Info; 5 | use Civix; 6 | use CRM\CivixBundle\Utils\Path; 7 | use Symfony\Component\Console\Command\Command; 8 | use Symfony\Component\Console\Input\InputInterface; 9 | use Symfony\Component\Console\Input\InputOption; 10 | use Symfony\Component\Console\Output\OutputInterface; 11 | 12 | abstract class AbstractCommand extends Command { 13 | 14 | protected function configure() { 15 | $this->addOption('yes', NULL, InputOption::VALUE_NONE, '(DEPRECATED) Alias for --no-interaction. All questions confirmed with their default choices.'); 16 | } 17 | 18 | protected function initialize(InputInterface $input, OutputInterface $output) { 19 | if ($input->hasOption('yes') && $input->getOption('yes')) { 20 | $output->writeln('<error>ALERT</error>: <comment>The option "--yes" is a deprecated alias. Please use "--no-interaction" or "-n".</comment>'); 21 | // IMHO, `--yes` is a more intuitive name. However, it implies that prompts are boolean, and it's 22 | // a little ambiguous what it means for multiple-choice questions. 23 | // By comparison, `--no-interaction` is the standardized name from Symfony Console. It's less ambiguous. 24 | // But at least 25 | $input->setOption('no-interaction', TRUE); 26 | $input->setInteractive(FALSE); 27 | } 28 | parent::initialize($input, $output); 29 | } 30 | 31 | public function run(InputInterface $input, OutputInterface $output) { 32 | try { 33 | \Civix::ioStack()->push($input, $output); 34 | return parent::run($input, $output); 35 | } 36 | finally { 37 | \Civix::ioStack()->pop(); 38 | } 39 | } 40 | 41 | protected function getModuleInfo(&$ctx): Info { 42 | $basedir = new Path(\CRM\CivixBundle\Application::findExtDir()); 43 | $info = new Info($basedir->string('info.xml')); 44 | $info->load($ctx); 45 | $attrs = $info->get()->attributes(); 46 | if ($attrs['type'] != 'module') { 47 | throw new \RuntimeException('Wrong extension type: ' . $attrs['type']); 48 | } 49 | return $info; 50 | } 51 | 52 | protected function assertCurrentFormat() { 53 | // Note: getModuleInfo() asserts that type is 'module' 54 | $info = $this->getModuleInfo($ctx); 55 | $actualVersion = $info->detectFormat(); 56 | $expectedVersion = Civix::upgradeList()->getHeadVersion(); 57 | if (version_compare($actualVersion, $expectedVersion, '<')) { 58 | throw new \Exception("This extension requires an upgrade for the file-format (current=$actualVersion; expected=$expectedVersion). Please run 'civix upgrade' before generating code."); 59 | } 60 | } 61 | 62 | } 63 | -------------------------------------------------------------------------------- /src/CRM/CivixBundle/Command/AddAngularModuleCommand.php: -------------------------------------------------------------------------------- 1 | <?php 2 | namespace CRM\CivixBundle\Command; 3 | 4 | use CRM\CivixBundle\Builder\Mixins; 5 | use Civix; 6 | use Symfony\Component\Console\Input\InputInterface; 7 | use Symfony\Component\Console\Input\InputOption; 8 | use Symfony\Component\Console\Output\OutputInterface; 9 | use CRM\CivixBundle\Builder\Collection; 10 | use CRM\CivixBundle\Builder\Dirs; 11 | use CRM\CivixBundle\Builder\PhpData; 12 | use CRM\CivixBundle\Builder\Template; 13 | use CRM\CivixBundle\Utils\Path; 14 | 15 | class AddAngularModuleCommand extends AbstractCommand { 16 | 17 | protected function configure() { 18 | parent::configure(); 19 | $this 20 | ->setName('generate:angular-module') 21 | ->setDescription('Add a new Angular module') 22 | ->addOption('am', NULL, InputOption::VALUE_REQUIRED, 'Name of the Angular module (default: match the Civi module name)'); 23 | } 24 | 25 | protected function execute(InputInterface $input, OutputInterface $output): int { 26 | $this->assertCurrentFormat(); 27 | 28 | //// Figure out template data //// 29 | $ctx = []; 30 | $ctx['type'] = 'module'; 31 | $ctx['basedir'] = \CRM\CivixBundle\Application::findExtDir(); 32 | $basedir = new Path($ctx['basedir']); 33 | $info = $this->getModuleInfo($ctx); 34 | 35 | $ctx['angularModuleName'] = $input->getOption('am') ? $input->getOption('am') : $ctx['angularModuleName']; 36 | $ctx['angularModulePhp'] = $basedir->string('ang', $ctx['angularModuleName'] . '.ang.php'); 37 | $ctx['angularModuleJs'] = $basedir->string('ang', $ctx['angularModuleName'] . '.js'); 38 | $ctx['angularModuleCss'] = $basedir->string('ang', $ctx['angularModuleName'] . '.css'); 39 | 40 | //// Construct files //// 41 | $output->writeln("<info>Initialize Angular module</info> " . $ctx['angularModuleName']); 42 | 43 | $ext = new Collection(); 44 | $ext->builders['dirs'] = new Dirs([ 45 | dirname($ctx['angularModulePhp']), 46 | dirname($ctx['angularModuleJs']), 47 | ]);; 48 | 49 | if (!file_exists($ctx['angularModulePhp'])) { 50 | $angModMeta = [ 51 | 'js' => [ 52 | 'ang/' . $ctx['angularModuleName'] . '.js', 53 | 'ang/' . $ctx['angularModuleName'] . '/*.js', 54 | 'ang/' . $ctx['angularModuleName'] . '/*/*.js', 55 | ], 56 | 'css' => [ 57 | 'ang/' . $ctx['angularModuleName'] . '.css', 58 | ], 59 | 'partials' => [ 60 | 'ang/' . $ctx['angularModuleName'], 61 | ], 62 | 'requires' => ['crmUi', 'crmUtil', 'ngRoute'], 63 | 'settings' => [], 64 | ]; 65 | $header = "// Angular module $ctx[angularModuleName].\n" 66 | . "// @see https://docs.civicrm.org/dev/en/latest/hooks/hook_civicrm_angularModules"; 67 | $ext->builders['ang.php'] = new PhpData($ctx['angularModulePhp'], $header); 68 | $ext->builders['ang.php']->set($angModMeta); 69 | } 70 | 71 | $ext->builders['js'] = new Template('angular-module.js.php', $ctx['angularModuleJs'], FALSE, Civix::templating()); 72 | $ext->builders['css'] = new Template('angular-module.css.php', $ctx['angularModuleCss'], FALSE, Civix::templating()); 73 | 74 | $ext->builders['mixins'] = new Mixins($info, $basedir->string('mixin'), ['ang-php@1.0']); 75 | $ext->builders['info'] = $info; 76 | 77 | $ext->loadInit($ctx); 78 | $ext->save($ctx, $output); 79 | return 0; 80 | } 81 | 82 | } 83 | -------------------------------------------------------------------------------- /src/CRM/CivixBundle/Command/AddCaseTypeCommand.php: -------------------------------------------------------------------------------- 1 | <?php 2 | namespace CRM\CivixBundle\Command; 3 | 4 | use CRM\CivixBundle\Builder\Mixins; 5 | use CRM\CivixBundle\Builder\Template; 6 | use Civix; 7 | use Symfony\Component\Console\Input\InputArgument; 8 | use Symfony\Component\Console\Input\InputInterface; 9 | use Symfony\Component\Console\Output\OutputInterface; 10 | use CRM\CivixBundle\Utils\Path; 11 | use Exception; 12 | 13 | class AddCaseTypeCommand extends AbstractCommand { 14 | 15 | protected function configure() { 16 | parent::configure(); 17 | $this 18 | ->setName('generate:case-type') 19 | ->setDescription('Add a CiviCase case-type') 20 | ->addArgument('<Label>', InputArgument::REQUIRED, 'Printable name of the case type') 21 | ->addArgument('<Name>', InputArgument::OPTIONAL, 'Code name of the case type (Default: Derive from <Label>)'); 22 | } 23 | 24 | protected function execute(InputInterface $input, OutputInterface $output): int { 25 | // load Civi to get access to civicrm_api_get_function_name 26 | Civix::boot(['output' => $output]); 27 | $civicrm_api3 = Civix::api3(); 28 | if (!$civicrm_api3 || !$civicrm_api3->local) { 29 | $output->writeln("Requires access to local CiviCRM source tree. Configure civicrm_api3_conf_path.</error>"); 30 | return 1; 31 | } 32 | 33 | $this->assertCurrentFormat(); 34 | 35 | if (!preg_match('/^[A-Z][A-Za-z0-9_ \.\-]*$/', $input->getArgument('<Label>'))) { 36 | throw new Exception("Label should be valid"); 37 | } 38 | if (!$input->getArgument('<Name>')) { 39 | // $input->setArgument('<Name>', \CRM_Utils_String::munge(ucwords(str_replace('_', ' ', $input->getArgument('<Label>'))), '', 0)); 40 | $input->setArgument('<Name>', \CRM_Case_XMLProcessor::mungeCasetype($input->getArgument('<Label>'))); 41 | } 42 | if (!preg_match('/^[A-Z][A-Za-z0-9]*$/', $input->getArgument('<Name>'))) { 43 | throw new Exception("Name should be valid (alphanumeric beginning with uppercase)"); 44 | } 45 | 46 | $ctx = []; 47 | $ctx['type'] = 'module'; 48 | $ctx['basedir'] = \CRM\CivixBundle\Application::findExtDir(); 49 | $ctx['caseTypeLabel'] = $input->getArgument('<Label>'); 50 | $ctx['caseTypeName'] = $input->getArgument('<Name>'); 51 | 52 | $basedir = new Path($ctx['basedir']); 53 | $info = $this->getModuleInfo($ctx); 54 | 55 | $xmlFile = $basedir->string('xml', 'case', $ctx['caseTypeName'] . '.xml'); 56 | $tpl = new Template('case-type.xml.php', $xmlFile, FALSE); 57 | $tpl->save($ctx, $output); 58 | 59 | $mixins = new Mixins($info, $basedir->string('mixin'), ['case-xml@1.0']); 60 | $mixins->save($ctx, $output); 61 | $info->save($ctx, $output); 62 | return 0; 63 | } 64 | 65 | } 66 | -------------------------------------------------------------------------------- /src/CRM/CivixBundle/Command/AddCustomDataCommand.php: -------------------------------------------------------------------------------- 1 | <?php 2 | namespace CRM\CivixBundle\Command; 3 | 4 | use Civix; 5 | use Symfony\Component\Console\Input\InputArgument; 6 | use Symfony\Component\Console\Input\InputInterface; 7 | use Symfony\Component\Console\Input\InputOption; 8 | use Symfony\Component\Console\Output\OutputInterface; 9 | use CRM\CivixBundle\Builder\Dirs; 10 | use CRM\CivixBundle\Builder\Info; 11 | use CRM\CivixBundle\Builder\CustomDataXML; 12 | use CRM\CivixBundle\Utils\Path; 13 | 14 | class AddCustomDataCommand extends AbstractCommand { 15 | 16 | protected function configure() { 17 | parent::configure(); 18 | $this 19 | ->setName('generate:custom-xml') 20 | ->setDescription('Export custom data and profiles to an XML file') 21 | ->addArgument('<CustomDataFile.xml>', InputArgument::OPTIONAL, 'The path to write custom data to (default: xml/auto_install.xml)') 22 | ->addOption('data', NULL, InputOption::VALUE_REQUIRED, 'Comma-separated list of custom data group IDs (from linked dev site)') 23 | ->addOption('uf', NULL, InputOption::VALUE_REQUIRED, 'Comma-separated list of profile group IDs (from linked dev site)') 24 | ->addOption('force', 'f', InputOption::VALUE_NONE, 'Overwrite existing files'); 25 | } 26 | 27 | protected function execute(InputInterface $input, OutputInterface $output): int { 28 | // load Civi to get access to civicrm_api_get_function_name 29 | Civix::boot(['output' => $output]); 30 | $civicrm_api3 = Civix::api3(); 31 | if (!$civicrm_api3 || !$civicrm_api3->local) { 32 | $output->writeln("<error>generate:custom-xml requires access to local CiviCRM instance. Configure civicrm_api3_conf_path.</error>"); 33 | return 1; 34 | } 35 | 36 | $this->assertCurrentFormat(); 37 | 38 | $ctx = []; 39 | $ctx['type'] = 'module'; 40 | $ctx['basedir'] = \CRM\CivixBundle\Application::findExtDir(); 41 | $basedir = new Path($ctx['basedir']); 42 | 43 | $info = new Info($basedir->string('info.xml')); 44 | $info->load($ctx); 45 | 46 | $dirs = new Dirs([ 47 | $basedir->string('xml'), 48 | ]); 49 | $dirs->save($ctx, $output); 50 | 51 | if ($input->getArgument('<CustomDataFile.xml>')) { 52 | $customDataXMLFile = $basedir->string($input->getArgument('<CustomDataFile.xml>')); 53 | } 54 | else { 55 | $customDataXMLFile = $basedir->string('xml', 'auto_install.xml'); 56 | } 57 | if (!$input->getOption('data') && !$input->getOption('uf')) { 58 | $output->writeln("<error>generate:custom-xml requires --data and/or --uf</error>"); 59 | return 1; 60 | } 61 | $customDataXML = new CustomDataXML( 62 | $input->getOption('data') ? explode(',', $input->getOption('data')) : [], 63 | $input->getOption('uf') ? explode(',', $input->getOption('uf')) : [], 64 | $customDataXMLFile, 65 | $input->getOption('force') 66 | ); 67 | $customDataXML->save($ctx, $output); 68 | 69 | if (preg_match('/\/xml\/.*_install.xml$/', $customDataXMLFile)) { 70 | $output->writeln(" * NOTE: This filename ends with \"_install.xml\". If you would like to load it automatically on new sites, then make sure there is an install/upgrade class (i.e. run \"civix generate:upgrader\")"); 71 | } 72 | else { 73 | $output->writeln(" * NOTE: By default, this file will not be loaded automatically -- you must define installation or upgrade logic to load the file."); 74 | } 75 | 76 | return 0; 77 | } 78 | 79 | } 80 | -------------------------------------------------------------------------------- /src/CRM/CivixBundle/Command/AddFormCommand.php: -------------------------------------------------------------------------------- 1 | <?php 2 | namespace CRM\CivixBundle\Command; 3 | 4 | use Symfony\Component\Console\Input\InputInterface; 5 | 6 | class AddFormCommand extends AbstractAddPageCommand { 7 | 8 | protected function configure() { 9 | parent::configure(); 10 | $this 11 | ->setName('generate:form') 12 | ->setDescription('Add a basic web form to a CiviCRM Module-Extension'); 13 | } 14 | 15 | protected function getPhpTemplate(InputInterface $input) { 16 | return 'form.php.php'; 17 | } 18 | 19 | protected function getTplTemplate(InputInterface $input) { 20 | return 'form.tpl.php'; 21 | } 22 | 23 | protected function createClassName(InputInterface $input, $ctx) { 24 | $namespace = str_replace('/', '_', $ctx['namespace']); 25 | return $namespace . '_Form_' . $ctx['shortClassName']; 26 | } 27 | 28 | protected function createTplName(InputInterface $input, $ctx) { 29 | return $ctx['namespace'] . '/Form/' . str_replace('_', '/', $ctx['shortClassName']) . '.tpl'; 30 | } 31 | 32 | } 33 | -------------------------------------------------------------------------------- /src/CRM/CivixBundle/Command/AddManagedEntityCommand.php: -------------------------------------------------------------------------------- 1 | <?php 2 | namespace CRM\CivixBundle\Command; 3 | 4 | use Civix; 5 | use Symfony\Component\Console\Input\InputArgument; 6 | use Symfony\Component\Console\Input\InputInterface; 7 | use Symfony\Component\Console\Output\OutputInterface; 8 | 9 | class AddManagedEntityCommand extends AbstractCommand { 10 | 11 | /** 12 | * Fields that most probably should be wrapped in E::ts() 13 | * @var array 14 | */ 15 | private $localizable = ['title', 'label', 'description', 'text']; 16 | 17 | protected function configure() { 18 | parent::configure(); 19 | $this 20 | ->setName('export') 21 | ->setDescription('Exports a record in packaged format for distribution in this extension') 22 | ->addArgument('<EntityName>', InputArgument::REQUIRED, 'API entity name (Ex: "SavedSearch")') 23 | ->addArgument('<EntityId>', InputArgument::REQUIRED, 'Id of entity to be exported (or name if exporting an Afform)') 24 | ->setHelp('Uses APIv4 Export to save existing records as .mgd.php files. 25 | Specify the name of the entity and the id. 26 | The file will be saved to the managed directory. 27 | 28 | This command also works to export Afforms to the ang directory. 29 | 30 | The command has some support for updating (re-exporting) managed records. 31 | '); 32 | } 33 | 34 | protected function execute(InputInterface $input, OutputInterface $output): int { 35 | $this->assertCurrentFormat(); 36 | 37 | $entityName = $input->getArgument('<EntityName>'); 38 | $entityId = $input->getArgument('<EntityId>'); 39 | 40 | // Boot CiviCRM to use api4 41 | Civix::boot(['output' => $output]); 42 | 43 | $gen = \Civix::generator(); 44 | $gen->addMixins(['mgd-php@1.0']); 45 | if ($entityName === 'Afform') { 46 | $gen->exportAfform($entityId); 47 | } 48 | else { 49 | $gen->exportMgd($entityName, $entityId); 50 | } 51 | 52 | return 0; 53 | } 54 | 55 | } 56 | -------------------------------------------------------------------------------- /src/CRM/CivixBundle/Command/AddPageCommand.php: -------------------------------------------------------------------------------- 1 | <?php 2 | namespace CRM\CivixBundle\Command; 3 | 4 | use Symfony\Component\Console\Input\InputInterface; 5 | 6 | class AddPageCommand extends AbstractAddPageCommand { 7 | 8 | protected function configure() { 9 | parent::configure(); 10 | $this 11 | ->setName('generate:page') 12 | ->setDescription('Add a basic web page to a CiviCRM Module-Extension'); 13 | } 14 | 15 | protected function getPhpTemplate(InputInterface $input) { 16 | return 'page.php.php'; 17 | } 18 | 19 | protected function getTplTemplate(InputInterface $input) { 20 | return 'page.tpl.php'; 21 | } 22 | 23 | protected function createClassName(InputInterface $input, $ctx) { 24 | $namespace = str_replace('/', '_', $ctx['namespace']); 25 | return $namespace . '_Page_' . $ctx['shortClassName']; 26 | } 27 | 28 | protected function createTplName(InputInterface $input, $ctx) { 29 | return $ctx['namespace'] . '/Page/' . $ctx['shortClassName'] . '.tpl'; 30 | } 31 | 32 | } 33 | -------------------------------------------------------------------------------- /src/CRM/CivixBundle/Command/AddServiceCommand.php: -------------------------------------------------------------------------------- 1 | <?php 2 | 3 | namespace CRM\CivixBundle\Command; 4 | 5 | use Civix; 6 | use CRM\CivixBundle\Utils\Naming; 7 | use Symfony\Component\Console\Input\InputArgument; 8 | use Symfony\Component\Console\Input\InputOption; 9 | use Symfony\Component\Console\Input\InputInterface; 10 | use Symfony\Component\Console\Output\OutputInterface; 11 | 12 | class AddServiceCommand extends AbstractCommand { 13 | 14 | protected function configure() { 15 | Civix::templating(); 16 | $this 17 | ->setName('generate:service') 18 | ->setDescription('Create a new service') 19 | ->addArgument('name', InputArgument::OPTIONAL, 'Machine-name for the service') 20 | ->addOption('naming', NULL, InputOption::VALUE_OPTIONAL, 'Force the service-class to use CRM- or Civi-style naming', 'auto') 21 | ->setHelp(''); 22 | parent::configure(); 23 | } 24 | 25 | protected function execute(InputInterface $input, OutputInterface $output): int { 26 | $gen = \Civix::generator(); 27 | $gen->addMixins(['scan-classes@1.0']); 28 | 29 | $servicePrefix = $gen->infoXml->getFile(); 30 | $namespace = Naming::coerceNamespace($gen->infoXml->getNamespace(), $input->getOption('naming')); 31 | 32 | if ($input->isInteractive()) { 33 | $defaultName = $input->getArgument('name') ?? Naming::createServiceName($servicePrefix, 'myService'); 34 | Civix::io()->note([ 35 | 'The service name is a short machine name. It may appear in contexts like:', 36 | sprintf('Civi::service("%s")->doSomething()', $defaultName), 37 | sprintf('It is recommended to always have a naming prefix (such as "%s").', $servicePrefix), 38 | ]); 39 | $serviceName = Civix::io()->ask('Service name', $defaultName, function ($answer) { 40 | if ('' === trim($answer)) { 41 | throw new \Exception('Service name cannot be empty'); 42 | } 43 | return $answer; 44 | }); 45 | } 46 | else { 47 | $serviceName = $input->getArgument('name'); 48 | if ('' === trim($serviceName)) { 49 | throw new \Exception('Service name cannot be empty'); 50 | } 51 | } 52 | 53 | $baseName = Naming::removeServicePrefix($servicePrefix, $serviceName); 54 | $baseNameParts = array_map('ucfirst', explode('.', $baseName)); 55 | $className = Naming::createClassName($namespace, ...$baseNameParts); 56 | 57 | $gen->addClass($className, 'service.php.php', [ 58 | 'service' => $serviceName, 59 | ], 'ask'); 60 | 61 | return 0; 62 | } 63 | 64 | } 65 | -------------------------------------------------------------------------------- /src/CRM/CivixBundle/Command/AddUpgraderCommand.php: -------------------------------------------------------------------------------- 1 | <?php 2 | namespace CRM\CivixBundle\Command; 3 | 4 | use Civix; 5 | use Symfony\Component\Console\Input\InputInterface; 6 | use Symfony\Component\Console\Output\OutputInterface; 7 | 8 | class AddUpgraderCommand extends AbstractCommand { 9 | 10 | protected function configure() { 11 | parent::configure(); 12 | $this 13 | ->setName('generate:upgrader') 14 | ->setDescription('Add an example upgrader class to a CiviCRM Module-Extension'); 15 | } 16 | 17 | protected function execute(InputInterface $input, OutputInterface $output): int { 18 | $this->assertCurrentFormat(); 19 | Civix::generator()->addUpgrader('if-forced'); 20 | return 0; 21 | } 22 | 23 | } 24 | -------------------------------------------------------------------------------- /src/CRM/CivixBundle/Command/BuildCommand.php: -------------------------------------------------------------------------------- 1 | <?php 2 | namespace CRM\CivixBundle\Command; 3 | 4 | use Symfony\Component\Console\Input\InputInterface; 5 | use Symfony\Component\Console\Output\OutputInterface; 6 | use Symfony\Component\Process\Process; 7 | use CRM\CivixBundle\Builder\Info; 8 | use CRM\CivixBundle\Utils\Path; 9 | 10 | class BuildCommand extends AbstractCommand { 11 | 12 | protected function configure() { 13 | parent::configure(); 14 | $this 15 | ->setName('build:zip') 16 | ->setDescription('Build a zip file for a CiviCRM Extension'); 17 | } 18 | 19 | protected function execute(InputInterface $input, OutputInterface $output): int { 20 | $this->assertCurrentFormat(); 21 | 22 | $ctx = []; 23 | $ctx['type'] = 'module'; 24 | $ctx['basedir'] = \CRM\CivixBundle\Application::findExtDir(); 25 | $basedir = new Path($ctx['basedir']); 26 | 27 | $info = new Info($basedir->string('info.xml')); 28 | $info->load($ctx); 29 | 30 | $ctx['zipFile'] = $basedir->string('build', $ctx['fullName'] . '.zip'); 31 | $cmdArgs = [ 32 | '-r', 33 | $ctx['zipFile'], 34 | $ctx['fullName'], 35 | '--exclude', 36 | 'build/*', 37 | '*~', 38 | '*.bak', 39 | '*.git*', 40 | ]; 41 | $cmd = 'zip ' . implode(' ', array_map('escapeshellarg', $cmdArgs)); 42 | 43 | chdir($basedir->string('..')); 44 | $process = new Process($cmd); 45 | $process->run(function ($type, $buffer) use ($output) { 46 | $output->writeln($buffer); 47 | }); 48 | if (!$process->isSuccessful()) { 49 | throw new \RuntimeException('Failed to create zip'); 50 | } 51 | print $process->getOutput(); 52 | return 0; 53 | } 54 | 55 | } 56 | -------------------------------------------------------------------------------- /src/CRM/CivixBundle/Command/ConfigGetCommand.php: -------------------------------------------------------------------------------- 1 | <?php 2 | namespace CRM\CivixBundle\Command; 3 | 4 | use Civix; 5 | use Symfony\Component\Console\Input\InputArgument; 6 | use Symfony\Component\Console\Input\InputInterface; 7 | use Symfony\Component\Console\Output\OutputInterface; 8 | 9 | class ConfigGetCommand extends AbstractCommand { 10 | 11 | protected function configure() { 12 | parent::configure(); 13 | $this 14 | ->setName('config:get') 15 | ->setDescription('Get configuration values') 16 | ->addArgument('parameter', InputArgument::OPTIONAL, 'Parameter name'); 17 | } 18 | 19 | protected function execute(InputInterface $input, OutputInterface $output): int { 20 | $config = Civix::config(); 21 | foreach ($this->getInterestingParameters() as $key) { 22 | printf("%-40s \"%s\"\n", $key, @$config['parameters'][$key]); 23 | } 24 | return 0; 25 | } 26 | 27 | protected function getInterestingParameters() { 28 | return [ 29 | 'author', 30 | 'email', 31 | 'license', 32 | ]; 33 | } 34 | 35 | } 36 | -------------------------------------------------------------------------------- /src/CRM/CivixBundle/Command/ConfigSetCommand.php: -------------------------------------------------------------------------------- 1 | <?php 2 | namespace CRM\CivixBundle\Command; 3 | 4 | use Civix; 5 | use Symfony\Component\Console\Input\InputArgument; 6 | use Symfony\Component\Console\Input\InputInterface; 7 | use Symfony\Component\Console\Output\OutputInterface; 8 | use CRM\CivixBundle\Builder\Collection; 9 | use CRM\CivixBundle\Builder\Ini; 10 | 11 | class ConfigSetCommand extends AbstractCommand { 12 | 13 | protected function configure() { 14 | parent::configure(); 15 | $this 16 | ->setName('config:set') 17 | ->setDescription('Set configuration value') 18 | ->addArgument('key', InputArgument::REQUIRED, 'Parameter name') 19 | ->addArgument('value', InputArgument::REQUIRED, 'Parameter value'); 20 | } 21 | 22 | protected function execute(InputInterface $input, OutputInterface $output): int { 23 | $ctx = []; 24 | $ext = new Collection(); 25 | 26 | $output->writeln("<info></info>"); 27 | $configDir = Civix::configDir(); 28 | $configDir->mkdir(); 29 | $ext->builders['ini'] = new Ini($configDir->string('civix.ini')); 30 | 31 | $ext->loadInit($ctx); 32 | $data = $ext->builders['ini']->get(); 33 | if (!is_array($data)) { 34 | $data = ['parameters' => []]; 35 | } 36 | $data['parameters'][$input->getArgument('key')] = $input->getArgument('value'); 37 | $ext->builders['ini']->set($data); 38 | $ext->save($ctx, $output); 39 | return 0; 40 | } 41 | 42 | } 43 | -------------------------------------------------------------------------------- /src/CRM/CivixBundle/Command/InfoGetCommand.php: -------------------------------------------------------------------------------- 1 | <?php 2 | namespace CRM\CivixBundle\Command; 3 | 4 | use Symfony\Component\Console\Command\Command; 5 | use Symfony\Component\Console\Input\InputInterface; 6 | use Symfony\Component\Console\Input\InputOption; 7 | use Symfony\Component\Console\Output\OutputInterface; 8 | use CRM\CivixBundle\Builder\Info; 9 | use CRM\CivixBundle\Utils\Path; 10 | 11 | class InfoGetCommand extends Command { 12 | 13 | protected function configure() { 14 | $fields = [ 15 | 'file', 16 | 'name', 17 | 'description', 18 | 'license', 19 | 'releaseDate', 20 | 'version', 21 | 'develStage', 22 | 'comments', 23 | 'maintainer/author', 24 | 'maintainer/email', 25 | 'civix/namespace', 26 | 'urls/url[@desc="Documentation"]', 27 | 'urls/url[@desc="Support"]', 28 | ]; 29 | 30 | $this 31 | ->setName('info:get') 32 | ->setDescription('Read a field from the info.xml file') 33 | ->addOption('xpath', 'x', InputOption::VALUE_REQUIRED, '(REQUIRED) The XPath expression of the field') 34 | ->setHelp("Read a single field from the info.xml file. 35 | 36 | Examples: 37 | civix info:get -x version 38 | civix info:get -x maintainer/author 39 | civix info:get -x maintainer/email 40 | 41 | Common fields:\n * " . implode("\n * ", $fields) . "\n"); 42 | } 43 | 44 | protected function execute(InputInterface $input, OutputInterface $output): int { 45 | $basedir = new Path(\CRM\CivixBundle\Application::findExtDir()); 46 | $info = new Info($basedir->string('info.xml')); 47 | $ctx = []; 48 | $info->load($ctx); 49 | $info->get(); 50 | 51 | $xpath = $input->getOption('xpath'); 52 | if (is_null($xpath)) { 53 | // missing xpath value so provide help 54 | $help = $this->getApplication()->get('help'); 55 | // tell help to provide specific help for this function 56 | $help->setCommand($this); 57 | return $help->run($input, $output); 58 | } 59 | foreach ($info->get()->xpath($xpath) as $node) { 60 | $output->writeln((string) $node, OutputInterface::OUTPUT_RAW); 61 | } 62 | 63 | return 0; 64 | } 65 | 66 | } 67 | -------------------------------------------------------------------------------- /src/CRM/CivixBundle/Command/InfoSetCommand.php: -------------------------------------------------------------------------------- 1 | <?php 2 | namespace CRM\CivixBundle\Command; 3 | 4 | use Symfony\Component\Console\Command\Command; 5 | use Symfony\Component\Console\Input\InputInterface; 6 | use Symfony\Component\Console\Input\InputOption; 7 | use Symfony\Component\Console\Output\OutputInterface; 8 | use CRM\CivixBundle\Builder\Info; 9 | use CRM\CivixBundle\Utils\Path; 10 | 11 | class InfoSetCommand extends Command { 12 | 13 | protected function configure() { 14 | $fields = [ 15 | 'file', 16 | 'name', 17 | 'description', 18 | 'license', 19 | 'releaseDate', 20 | 'version', 21 | 'develStage', 22 | 'comments', 23 | 'maintainer/author', 24 | 'maintainer/email', 25 | 'civix/namespace', 26 | 'urls/url[@desc="Documentation"]', 27 | 'urls/url[@desc="Support"]', 28 | ]; 29 | 30 | $this 31 | ->setName('info:set') 32 | ->setDescription('Set a field in the info.xml file') 33 | ->addOption('xpath', 'x', InputOption::VALUE_REQUIRED, 'The XPath expression to search on') 34 | ->addOption('to', 't', InputOption::VALUE_REQUIRED, 'The value of the field') 35 | ->setHelp("Set a single field in the info.xml file. 36 | 37 | Examples: 38 | civix info:set -x version --to 2.0.0 39 | civix info:set -x maintainer/author --to 'Frank Lloyd Wright' 40 | civix info:set -x maintainer/email --to 'flloyd@example.com' 41 | 42 | Common fields: 43 | * " . implode("\n * ", $fields) . "\n"); 44 | } 45 | 46 | protected function execute(InputInterface $input, OutputInterface $output): int { 47 | $basedir = new Path(\CRM\CivixBundle\Application::findExtDir()); 48 | $info = new Info($basedir->string('info.xml')); 49 | $ctx = []; 50 | $info->load($ctx); 51 | $info->get(); 52 | 53 | $elements = $info->get()->xpath($input->getOption('xpath')); 54 | if (empty($elements)) { 55 | $output->writeln("Error: Path (" . $input->getOption('xpath') . ") did not match any elements."); 56 | return 1; 57 | } 58 | foreach ($elements as $element) { 59 | $element[0] = $input->getOption('to'); 60 | } 61 | 62 | $info->save($ctx, $output); 63 | return 0; 64 | } 65 | 66 | } 67 | -------------------------------------------------------------------------------- /src/CRM/CivixBundle/Command/Mgd.php: -------------------------------------------------------------------------------- 1 | <?php 2 | 3 | namespace CRM\CivixBundle\Command; 4 | 5 | use CRM\CivixBundle\Utils\Files; 6 | 7 | class Mgd { 8 | 9 | public static function assertManageableEntity(string $entityName, $id, string $extKey, string $managedName, string $managedFileName): void { 10 | $io = \Civix::io(); 11 | $existingMgd = \civicrm_api4('Managed', 'get', [ 12 | 'select' => ['module', 'name', 'id'], 13 | 'where' => [ 14 | ['entity_type', '=', $entityName], 15 | ['entity_id', '=', $id], 16 | ], 17 | 'checkPermissions' => FALSE, 18 | ])->first(); 19 | if ($existingMgd) { 20 | if ($existingMgd['module'] !== $extKey || $existingMgd['name'] !== $managedName) { 21 | $io->warning([ 22 | sprintf("Requested entity (%s) is already managed by \"%s\" (#%s). Adding new entity \"%s\" would create conflict.", 23 | "$entityName $id", 24 | $existingMgd['module'] . ':' . $existingMgd['name'], 25 | $existingMgd['id'], 26 | "$extKey:$managedName" 27 | ), 28 | ]); 29 | } 30 | if (!file_exists($managedFileName)) { 31 | $io->warning([ 32 | sprintf('The managed entity (%s) already exists in the database, but the expected file (%s) does not exist.', 33 | "$extKey:$managedName", 34 | Files::relativize($managedFileName, \CRM\CivixBundle\Application::findExtDir()) 35 | ), 36 | 'The new file will be created, but you may have a conflict within this extension.', 37 | ]); 38 | } 39 | } 40 | } 41 | 42 | } 43 | -------------------------------------------------------------------------------- /src/CRM/CivixBundle/Command/PingCommand.php: -------------------------------------------------------------------------------- 1 | <?php 2 | namespace CRM\CivixBundle\Command; 3 | 4 | use Civix; 5 | use Symfony\Component\Console\Input\InputInterface; 6 | use Symfony\Component\Console\Output\OutputInterface; 7 | 8 | class PingCommand extends AbstractCommand { 9 | 10 | protected function configure() { 11 | parent::configure(); 12 | $this 13 | ->setName('civicrm:ping') 14 | ->setDescription('Test whether the CiviCRM client is properly configured'); 15 | } 16 | 17 | protected function execute(InputInterface $input, OutputInterface $output): int { 18 | Civix::boot(['output' => $output]); 19 | $civicrm_api3 = Civix::api3(); 20 | if ($civicrm_api3->Contact->Get(['option.limit' => 1])) { 21 | if (empty($civicrm_api3->result->values[0]->contact_type)) { 22 | $output->writeln('<error>Ping failed: Site reported that it found no contacts</error>'); 23 | } 24 | else { 25 | $output->writeln('<info>Ping successful</info>'); 26 | } 27 | } 28 | else { 29 | $output->writeln('<error>Ping failed: API Error: ' . $civicrm_api3->errorMsg() . '</error>'); 30 | } 31 | return 0; 32 | } 33 | 34 | } 35 | -------------------------------------------------------------------------------- /src/CRM/CivixBundle/ComposerCompile.php: -------------------------------------------------------------------------------- 1 | <?php 2 | namespace CRM\CivixBundle; 3 | 4 | /** 5 | * Helper methods called via `composer compile`. 6 | * 7 | * Note that the application is still being setup, so it may be ill-advised to use higher-level services. 8 | */ 9 | class ComposerCompile { 10 | 11 | /** 12 | * Download the mixin files 13 | * 14 | * Note: We want to read the list from `mixin-backports.php` because we can track extra metadata. 15 | * 16 | * @param array $task 17 | */ 18 | public static function downloadMixins(array $task): void { 19 | $civix = dirname(__DIR__, 3); 20 | $mixinListFile = "$civix/mixin-backports.php"; 21 | $mixinListTimestamp = filemtime($mixinListFile); 22 | $mixins = require $mixinListFile; 23 | foreach ($mixins as $id => $mixin) { 24 | $checksum = file_exists($mixin['local']) ? hash('sha256', file_get_contents($mixin['local'])) : ''; 25 | 26 | if (!file_exists($mixin['local']) || $checksum !== $mixin['sha256']) { 27 | printf(" - Download %s\n", $mixin['local']); 28 | $content = file_get_contents($mixin['remote']); 29 | $contentChecksum = hash('sha256', $content); 30 | if ($contentChecksum !== $mixin['sha256']) { 31 | throw new \RuntimeException("Download from {$mixin['remote']} has wrong checksum. (expect={$mixin['sha256']}, actual=$contentChecksum)"); 32 | } 33 | 34 | static::putFile($mixin['local'], $content); 35 | } 36 | } 37 | } 38 | 39 | protected static function putFile(string $path, string $content): void { 40 | $outDir = dirname($path); 41 | if (!is_dir($outDir)) { 42 | if (!mkdir($outDir, 0777, TRUE)) { 43 | throw new \RuntimeException("Failed to make dir: $outDir"); 44 | } 45 | } 46 | if (FALSE === file_put_contents($path, $content)) { 47 | throw new \RuntimeException("Failed to write file: $path"); 48 | } 49 | } 50 | 51 | } 52 | -------------------------------------------------------------------------------- /src/CRM/CivixBundle/Parse/ParseException.php: -------------------------------------------------------------------------------- 1 | <?php 2 | 3 | namespace CRM\CivixBundle\Parse; 4 | 5 | class ParseException extends \Exception { 6 | } 7 | -------------------------------------------------------------------------------- /src/CRM/CivixBundle/Parse/Token.php: -------------------------------------------------------------------------------- 1 | <?php 2 | 3 | namespace CRM\CivixBundle\Parse; 4 | 5 | if (!defined('TX_RAW')) { 6 | define('TX_RAW', -1); 7 | } 8 | 9 | /** 10 | * OOP wrapper of PHP's token. 11 | * 12 | * For writing recursive-descent parsers, it can get a little tedious 13 | * to constantly guard against `is_array()/is_string()`. 14 | */ 15 | class Token { 16 | 17 | public static function tokenize(string $code): array { 18 | $raw = token_get_all($code); 19 | return array_map(function ($t) { 20 | return is_array($t) ? new Token($t[0], $t[1]) : new Token(TX_RAW, $t); 21 | }, $raw); 22 | } 23 | 24 | /** 25 | * @param $typeId 26 | * @param $value 27 | */ 28 | public function __construct(int $typeId, string $value) { 29 | $this->typeId = $typeId; 30 | $this->value = $value; 31 | } 32 | 33 | protected $typeId; 34 | protected $value; 35 | 36 | /** 37 | * Get ID code of the token type. 38 | * 39 | * @return int 40 | * Ex: T_FUNCTION, T_WHITESPACE, TX_RAW 41 | */ 42 | public function typeId(): int { 43 | return $this->typeId; 44 | } 45 | 46 | /** 47 | * Get symbolic name of the token type. 48 | * 49 | * @return string 50 | * Ex: 'T_FUNCTION', 'T_WHITESPACE_', 'TX_RAW' 51 | */ 52 | public function name(): string { 53 | if ($this->typeId === TX_RAW) { 54 | return 'TX_RAW'; 55 | } 56 | else { 57 | return token_name($this->typeId); 58 | } 59 | } 60 | 61 | /** 62 | * Get literal value of token. 63 | * 64 | * @return string 65 | */ 66 | public function value(): string { 67 | return $this->value; 68 | } 69 | 70 | /** 71 | * Check if the token matches an expectation 72 | * 73 | * @param $expect 74 | * One of: 75 | * - Integer: The token-type ID 76 | * - String: The raw value of the token 77 | * - Array: List of strings or integers; any one to match 78 | * @return bool 79 | */ 80 | public function is($expect): bool { 81 | if ($expect === NULL) { 82 | return TRUE; /* wildcard */ 83 | } 84 | if (is_array($expect)) { 85 | foreach ($expect as $expectOption) { 86 | if ($this->is($expectOption)) { 87 | return TRUE; 88 | } 89 | } 90 | return FALSE; 91 | } 92 | if (is_int($expect)) { 93 | return $this->typeId === $expect; 94 | } 95 | if (is_string($expect)) { 96 | return $this->value === $expect; 97 | } 98 | throw new ParseException("Token::is() expects type ID or literal value"); 99 | } 100 | 101 | public function assert($expect): void { 102 | if ($this->is($expect)) { 103 | return; 104 | } 105 | 106 | if ($expect === TX_RAW) { 107 | $expectPretty = 'TX_RAW'; 108 | } 109 | elseif (is_int($expect)) { 110 | $expectPretty = token_name($expect); 111 | } 112 | else { 113 | $expectPretty = json_encode($expect, JSON_UNESCAPED_SLASHES); 114 | } 115 | 116 | throw new ParseException(sprintf('Token %s does not match expectation %s', $this->__toString(), $expectPretty)); 117 | } 118 | 119 | public function __toString(): string { 120 | return sprintf("%s [%s] %s", $this->name(), $this->typeId, json_encode($this->value(), JSON_UNESCAPED_SLASHES)); 121 | } 122 | 123 | } 124 | -------------------------------------------------------------------------------- /src/CRM/CivixBundle/Resources/doc/index.rst: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/totten/civix/b4c7e5fe832cb88a91b503a6f7ea436e7637f749/src/CRM/CivixBundle/Resources/doc/index.rst -------------------------------------------------------------------------------- /src/CRM/CivixBundle/Resources/translations/messages.fr.xlf: -------------------------------------------------------------------------------- 1 | <?xml version="1.0"?> 2 | <xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2"> 3 | <file source-language="en" datatype="plaintext" original="file.ext"> 4 | <body> 5 | <trans-unit id="1"> 6 | <source>Symfony2 is great</source> 7 | <target>J'aime Symfony2</target> 8 | </trans-unit> 9 | </body> 10 | </file> 11 | </xliff> 12 | -------------------------------------------------------------------------------- /src/CRM/CivixBundle/Resources/views/Code/angular-dir.html.php: -------------------------------------------------------------------------------- 1 | <div> 2 | <p>{{ts('Example directive (<?php echo $dirNameHyp ?>)')}}</p> 3 | <pre>Options: {{myOptions|json}}</pre> 4 | </div> 5 | -------------------------------------------------------------------------------- /src/CRM/CivixBundle/Resources/views/Code/angular-dir.js.php: -------------------------------------------------------------------------------- 1 | (function(angular, $, _) { 2 | // "<?php echo $dirNameCamel ?>" is a basic skeletal directive. 3 | // Example usage: <div <?php echo $dirNameHyp ?>="{foo: 1, bar: 2}"></div> 4 | angular.module('<?php echo $angularModuleName ?>').directive('<?php echo $dirNameCamel ?>', function() { 5 | return { 6 | restrict: 'AE', 7 | templateUrl: '<?php echo $htmlName ?>', 8 | scope: { 9 | <?php echo $dirNameCamel ?>: '=' 10 | }, 11 | link: function($scope, $el, $attr) { 12 | var ts = $scope.ts = CRM.ts('<?php echo $tsDomain ?>'); 13 | $scope.$watch('<?php echo $dirNameCamel ?>', function(newValue){ 14 | $scope.myOptions = newValue; 15 | }); 16 | } 17 | }; 18 | }); 19 | })(angular, CRM.$, CRM._); 20 | -------------------------------------------------------------------------------- /src/CRM/CivixBundle/Resources/views/Code/angular-module.css.php: -------------------------------------------------------------------------------- 1 | /* Add any CSS rules for Angular module "<?php echo $angularModuleName ?>" */ 2 | -------------------------------------------------------------------------------- /src/CRM/CivixBundle/Resources/views/Code/angular-module.js.php: -------------------------------------------------------------------------------- 1 | (function(angular, $, _) { 2 | // Declare a list of dependencies. 3 | angular.module('<?php echo $angularModuleName ?>', CRM.angRequires('<?php echo $angularModuleName ?>')); 4 | })(angular, CRM.$, CRM._); 5 | -------------------------------------------------------------------------------- /src/CRM/CivixBundle/Resources/views/Code/angular-page.hlp.php: -------------------------------------------------------------------------------- 1 | {htxt id="full_name"} 2 | {ts}The contact name should be divided in two parts, the first name and last name.{/ts} 3 | {/htxt} 4 | -------------------------------------------------------------------------------- /src/CRM/CivixBundle/Resources/views/Code/angular-page.html.php: -------------------------------------------------------------------------------- 1 | <div class="crm-container"> 2 | <div crm-ui-debug="$ctrl.myContact"></div> 3 | 4 | <h1 crm-page-title>{{ts('About %1', {1: $ctrl.myContact.first_name + ' ' + $ctrl.myContact.last_name})}}</h1> 5 | 6 | <form name="myForm" crm-ui-id-scope> 7 | 8 | <div class="help"> 9 | <p>{{ts('This is an auto-generated skeletal page.')}}</p> 10 | 11 | <p>{{ts('To view more debugging information, edit the URL and include "?angularDebug=1".')}}</p> 12 | </div> 13 | 14 | <div crm-ui-accordion="{title: ts('Greeting')}"> 15 | <p ng-show="$ctrl.myContact.first_name">{{ ts('Hello, %1.', {1: $ctrl.myContact.first_name}) }}</p> 16 | 17 | <p ng-show="!$ctrl.myContact.first_name">{{ ts('Hello, stranger.') }}</p> 18 | </div> 19 | 20 | <div crm-ui-accordion="{title: ts('About Me')}"> 21 | <div class="crm-block"> 22 | <div class="crm-group"> 23 | <div crm-ui-field="{name: 'myForm.first_name', title: ts('Name'), help: hs('full_name')}"> 24 | <input 25 | crm-ui-id="myForm.first_name" 26 | name="first_name" 27 | ng-model="$ctrl.myContact.first_name" 28 | class="crm-form-text" 29 | placeholder="{{ts('First')}}" 30 | /> 31 | <input 32 | crm-ui-id="myForm.last_name" 33 | name="last_name" 34 | ng-model="$ctrl.myContact.last_name" 35 | class="crm-form-text" 36 | placeholder="{{ts('Last')}}" 37 | /> 38 | </div> 39 | </div> 40 | </div> 41 | 42 | <div> 43 | <button ng-click="$ctrl.save()">{{ts('Save')}}</button> 44 | </div> 45 | </div> 46 | 47 | </form> 48 | 49 | </div> 50 | -------------------------------------------------------------------------------- /src/CRM/CivixBundle/Resources/views/Code/angular-page.js.php: -------------------------------------------------------------------------------- 1 | (function(angular, $, _) { 2 | 3 | angular.module('<?php echo $angularModuleName ?>').config(function($routeProvider) { 4 | $routeProvider.when('/<?php echo $ctrlRelPath ?>', { 5 | controller: '<?php echo $ctrlName ?>', 6 | controllerAs: '$ctrl', 7 | templateUrl: '<?php echo $htmlName ?>', 8 | 9 | // If you need to look up data when opening the page, list it out 10 | // under "resolve". 11 | resolve: { 12 | myContact: function(crmApi) { 13 | return crmApi('Contact', 'getsingle', { 14 | id: 'user_contact_id', 15 | return: ['first_name', 'last_name'] 16 | }); 17 | } 18 | } 19 | }); 20 | } 21 | ); 22 | 23 | // The controller uses *injection*. This default injects a few things: 24 | // $scope -- This is the set of variables shared between JS and HTML. 25 | // crmApi, crmStatus, crmUiHelp -- These are services provided by civicrm-core. 26 | // myContact -- The current contact, defined above in config(). 27 | angular.module('<?php echo $angularModuleName ?>').controller('<?php echo $ctrlName ?>', function($scope, crmApi, crmStatus, crmUiHelp, myContact) { 28 | // The ts() and hs() functions help load strings for this module. 29 | var ts = $scope.ts = CRM.ts('<?php echo $tsDomain ?>'); 30 | var hs = $scope.hs = crmUiHelp({file: '<?php echo $hlpName ?>'}); // See: templates/<?php echo $hlpName ?>.hlp 31 | // Local variable for this controller (needed when inside a callback fn where `this` is not available). 32 | var ctrl = this; 33 | 34 | // We have myContact available in JS. We also want to reference it in HTML. 35 | this.myContact = myContact; 36 | 37 | this.save = function() { 38 | return crmStatus( 39 | // Status messages. For defaults, just use "{}" 40 | {start: ts('Saving...'), success: ts('Saved')}, 41 | // The save action. Note that crmApi() returns a promise. 42 | crmApi('Contact', 'create', { 43 | id: ctrl.myContact.id, 44 | first_name: ctrl.myContact.first_name, 45 | last_name: ctrl.myContact.last_name 46 | }) 47 | ); 48 | }; 49 | }); 50 | 51 | })(angular, CRM.$, CRM._); 52 | -------------------------------------------------------------------------------- /src/CRM/CivixBundle/Resources/views/Code/api.php.php: -------------------------------------------------------------------------------- 1 | <?php 2 | echo "<?php\n"; 3 | $_namespace = preg_replace(':/:', '_', $namespace); 4 | ?> 5 | use <?php echo $_namespace ?>_ExtensionUtil as E; 6 | 7 | /** 8 | * <?php echo $entityNameCamel ?>.<?php echo $actionNameCamel ?> API specification (optional) 9 | * This is used for documentation and validation. 10 | * 11 | * @param array $spec description of fields supported by this API call 12 | * 13 | * @see https://docs.civicrm.org/dev/en/latest/framework/api-architecture/ 14 | */ 15 | function _<?php echo $apiFunction ?>_spec(&$spec) { 16 | $spec['magicword']['api.required'] = 1; 17 | } 18 | 19 | /** 20 | * <?php echo $entityNameCamel ?>.<?php echo $actionNameCamel ?> API 21 | * 22 | * @param array $params 23 | * 24 | * @return array 25 | * API result descriptor 26 | * 27 | * @see civicrm_api3_create_success 28 | * 29 | * @throws CRM_Core_Exception 30 | */ 31 | function <?php echo $apiFunction ?>($params) { 32 | if (array_key_exists('magicword', $params) && $params['magicword'] == 'sesame') { 33 | $returnValues = array( 34 | // OK, return several data rows 35 | 12 => ['id' => 12, 'name' => 'Twelve'], 36 | 34 => ['id' => 34, 'name' => 'Thirty four'], 37 | 56 => ['id' => 56, 'name' => 'Fifty six'], 38 | ); 39 | // ALTERNATIVE: $returnValues = []; // OK, success 40 | // ALTERNATIVE: $returnValues = ["Some value"]; // OK, return a single value 41 | 42 | // Spec: civicrm_api3_create_success($values = 1, $params = [], $entity = NULL, $action = NULL) 43 | return civicrm_api3_create_success($returnValues, $params, '<?php echo $entityNameCamel; ?>', '<?php echo $actionNameCamel; ?>'); 44 | } 45 | else { 46 | throw new CRM_Core_Exception(/*error_message*/ 'Everyone knows that the magicword is "sesame"', /*error_code*/ 'magicword_incorrect'); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/CRM/CivixBundle/Resources/views/Code/bootstrap.css.php: -------------------------------------------------------------------------------- 1 | /** 2 | * This is a placeholder for bootstrap.css in the '<?php echo $themeName; ?>' theme. 3 | * Here are a few ideas on what to do about it: 4 | * - Make a real CSS file (e.g. by hand-coding or setting up a SASS/SCSS build process). 5 | * - Inherit the stock Greenwich file: 6 | * - Edit the file '<?php echo $themeName; ?>.theme.php' 7 | * and add 'greenwich' to the 'search_order' 8 | * - Delete this copy of 'bootstrap.css' 9 | * - Disable this file completely: 10 | * - Edit the file '<?php echo $themeName; ?>.theme.php' 11 | * and add 'css/bootstrap.css' to the 'excludes' list 12 | * - Delete this copy of 'bootstrap.css' 13 | */ 14 | -------------------------------------------------------------------------------- /src/CRM/CivixBundle/Resources/views/Code/case-type.xml.php: -------------------------------------------------------------------------------- 1 | <?php echo '<?xml version="1.0" encoding="iso-8859-1" ?>'; ?> 2 | <CaseType> 3 | <name><?php echo $caseTypeLabel?></name> 4 | <description><?php echo $caseTypeLabel?></description> 5 | 6 | <ActivityTypes> 7 | 8 | <!-- Standard case-management activities --> 9 | <ActivityType> 10 | <name>Open Case</name> 11 | <max_instances>1</max_instances> 12 | </ActivityType> 13 | <ActivityType> 14 | <name>Follow up</name> 15 | </ActivityType> 16 | <ActivityType> 17 | <name>Change Case Type</name> 18 | </ActivityType> 19 | <ActivityType> 20 | <name>Change Case Status</name> 21 | </ActivityType> 22 | <ActivityType> 23 | <name>Change Case Start Date</name> 24 | </ActivityType> 25 | <ActivityType> 26 | <name>Link Cases</name> 27 | </ActivityType> 28 | 29 | <!-- Configurable activities --> 30 | <ActivityType> 31 | <name>Email</name> 32 | </ActivityType> 33 | <ActivityType> 34 | <name>Meeting</name> 35 | </ActivityType> 36 | <ActivityType> 37 | <name>Phone Call</name> 38 | </ActivityType> 39 | 40 | </ActivityTypes> 41 | 42 | <ActivitySets> 43 | <ActivitySet> 44 | <name>standard_timeline</name> 45 | <label>Standard Timeline</label> 46 | <timeline>true</timeline> 47 | <ActivityTypes> 48 | <ActivityType> 49 | <name>Open Case</name> 50 | <status>Completed</status> 51 | </ActivityType> 52 | <ActivityType> 53 | <name>Phone Call</name> 54 | <reference_offset>1</reference_offset> 55 | <reference_select>newest</reference_select> 56 | </ActivityType> 57 | <ActivityType> 58 | <name>Follow up</name> 59 | <reference_offset>7</reference_offset> 60 | <reference_select>newest</reference_select> 61 | </ActivityType> 62 | </ActivityTypes> 63 | </ActivitySet> 64 | </ActivitySets> 65 | 66 | <CaseRoles> 67 | <RelationshipType> 68 | <name>Case Coordinator</name> 69 | <creator>1</creator> 70 | <manager>1</manager> 71 | </RelationshipType> 72 | </CaseRoles> 73 | 74 | </CaseType> 75 | -------------------------------------------------------------------------------- /src/CRM/CivixBundle/Resources/views/Code/civicrm.css.php: -------------------------------------------------------------------------------- 1 | /** 2 | * This is a placeholder for civicrm.css in the '<?php echo $themeName; ?>' theme. 3 | * Here are a few ideas on what to do about it: 4 | * - Make a real CSS file (e.g. by hand-coding or setting up a SASS/SCSS build process). 5 | * - Inherit the stock Greenwich file: 6 | * - Edit the file '<?php echo $themeName; ?>.theme.php' 7 | * and add 'greenwich' to the 'search_order' 8 | * - Delete this copy of 'civicrm.css' 9 | * - Disable this file completely: 10 | * - Edit the file '<?php echo $themeName; ?>.theme.php' 11 | * and add 'css/civicrm.css' to the 'excludes' list 12 | * - Delete this copy of 'civicrm.css' 13 | */ 14 | -------------------------------------------------------------------------------- /src/CRM/CivixBundle/Resources/views/Code/entity-api.php.php: -------------------------------------------------------------------------------- 1 | <?php 2 | echo "<?php\n"; 3 | $_namespace = preg_replace(':/:', '_', $namespace); 4 | ?> 5 | use <?php echo $_namespace ?>_ExtensionUtil as E; 6 | 7 | /** 8 | * <?php echo $entityNameCamel ?>.create API specification (optional). 9 | * This is used for documentation and validation. 10 | * 11 | * @param array $spec description of fields supported by this API call 12 | * 13 | * @see https://docs.civicrm.org/dev/en/latest/framework/api-architecture/ 14 | */ 15 | function _<?php echo $apiFunctionPrefix ?>create_spec(&$spec) { 16 | // $spec['some_parameter']['api.required'] = 1; 17 | } 18 | 19 | /** 20 | * <?php echo $entityNameCamel ?>.create API. 21 | * 22 | * @param array $params 23 | * 24 | * @return array 25 | * API result descriptor 26 | * 27 | * @throws CRM_Core_Exception 28 | */ 29 | function <?php echo $apiFunctionPrefix ?>create($params) { 30 | return _civicrm_api3_basic_create(_civicrm_api3_get_BAO(__FUNCTION__), $params, <?php var_export($entityNameCamel); ?>); 31 | } 32 | 33 | /** 34 | * <?php echo $entityNameCamel ?>.delete API. 35 | * 36 | * @param array $params 37 | * 38 | * @return array 39 | * API result descriptor 40 | * 41 | * @throws CRM_Core_Exception 42 | */ 43 | function <?php echo $apiFunctionPrefix ?>delete($params) { 44 | return _civicrm_api3_basic_delete(_civicrm_api3_get_BAO(__FUNCTION__), $params); 45 | } 46 | 47 | /** 48 | * <?php echo $entityNameCamel ?>.get API. 49 | * 50 | * @param array $params 51 | * 52 | * @return array 53 | * API result descriptor 54 | * 55 | * @throws CRM_Core_Exception 56 | */ 57 | function <?php echo $apiFunctionPrefix ?>get($params) { 58 | return _civicrm_api3_basic_get(_civicrm_api3_get_BAO(__FUNCTION__), $params, TRUE, <?php var_export($entityNameCamel); ?>); 59 | } 60 | -------------------------------------------------------------------------------- /src/CRM/CivixBundle/Resources/views/Code/entity-api3-test.php.php: -------------------------------------------------------------------------------- 1 | <?php 2 | echo "<?php\n"; 3 | ?> 4 | 5 | use Civi\Test\CiviEnvBuilder; 6 | use Civi\Test\HeadlessInterface; 7 | use Civi\Test\HookInterface; 8 | use Civi\Test\TransactionalInterface; 9 | 10 | /** 11 | * <?php echo $entityNameCamel ?> API Test Case 12 | * @group headless 13 | */ 14 | class <?php echo $testApi3ClassName ?> extends \PHPUnit\Framework\TestCase implements HeadlessInterface, HookInterface, TransactionalInterface { 15 | use \Civi\Test\Api3TestTrait; 16 | 17 | /** 18 | * Set up for headless tests. 19 | * 20 | * Civi\Test has many helpers, like install(), uninstall(), sql(), and sqlFile(). 21 | * 22 | * See: https://docs.civicrm.org/dev/en/latest/testing/phpunit/#civitest 23 | */ 24 | public function setUpHeadless(): CiviEnvBuilder { 25 | return \Civi\Test::headless() 26 | ->installMe(__DIR__) 27 | ->apply(); 28 | } 29 | 30 | /** 31 | * The setup() method is executed before the test is executed (optional). 32 | */ 33 | public function setUp(): void { 34 | $table = CRM_Core_DAO_AllCoreTables::getTableForEntityName(<?php var_export($entityNameCamel); ?>); 35 | $this->assertTrue($table && CRM_Core_DAO::checkTableExists($table), 'There was a problem with extension installation. Table for ' . <?php var_export($entityNameCamel); ?> . ' not found.'); 36 | parent::setUp(); 37 | } 38 | 39 | /** 40 | * The tearDown() method is executed after the test was executed (optional) 41 | * This can be used for cleanup. 42 | */ 43 | public function tearDown(): void { 44 | parent::tearDown(); 45 | } 46 | 47 | /** 48 | * Simple example test case. 49 | * 50 | * Note how the function name begins with the word "test". 51 | */ 52 | public function testCreateGetDelete(): void { 53 | // Boilerplate entity has one data field -- 'contact_id'. 54 | // Put some data in, read it back out, and delete it. 55 | 56 | $created = $this->callAPISuccess(<?php var_export($entityNameCamel); ?>, 'create', [ 57 | 'contact_id' => 1, 58 | ]); 59 | $this->assertTrue(is_numeric($created['id'])); 60 | 61 | $get = $this->callAPISuccess(<?php var_export($entityNameCamel); ?>, 'get', []); 62 | $this->assertEquals(1, $get['count']); 63 | $this->assertEquals(1, $get['values'][$created['id']]['contact_id']); 64 | 65 | $this->callAPISuccess(<?php var_export($entityNameCamel); ?>, 'delete', [ 66 | 'id' => $created['id'], 67 | ]); 68 | } 69 | 70 | } 71 | -------------------------------------------------------------------------------- /src/CRM/CivixBundle/Resources/views/Code/entity-api4.php.php: -------------------------------------------------------------------------------- 1 | <?php echo "<?php\n"; ?> 2 | namespace Civi\Api4; 3 | 4 | /** 5 | * <?php echo $entityNameCamel ?> entity. 6 | * 7 | * Provided by the <?php echo $extensionName ?> extension. 8 | * 9 | * @package Civi\Api4 10 | */ 11 | class <?php echo $entityNameCamel ?> extends Generic\DAOEntity { 12 | 13 | } 14 | -------------------------------------------------------------------------------- /src/CRM/CivixBundle/Resources/views/Code/entity-bao.php.php: -------------------------------------------------------------------------------- 1 | <?php 2 | echo "<?php\n"; 3 | $_namespace = preg_replace(':/:', '_', $namespace); 4 | ?> 5 | 6 | use <?php echo $_namespace ?>_ExtensionUtil as E; 7 | 8 | class <?php echo $baoClassName ?> extends <?php echo $daoClassName ?> { 9 | 10 | } 11 | -------------------------------------------------------------------------------- /src/CRM/CivixBundle/Resources/views/Code/entity-dao.php.php: -------------------------------------------------------------------------------- 1 | <?php 2 | echo "<" . "?php\n"; 3 | if ($classNamespaceDecl) { 4 | echo "$classNamespaceDecl\n\n"; 5 | } 6 | ?> 7 | 8 | /** 9 | * DAOs provide an OOP-style facade for reading and writing database records. 10 | * 11 | * DAOs are a primary source for metadata in older versions of CiviCRM (<5.74) 12 | * and are required for some subsystems (such as APIv3). 13 | * 14 | * This stub provides compatibility. It is not intended to be modified in a 15 | * substantive way. Property annotations may be added, but are not required. 16 | <?php 17 | foreach ($properties as $propName => $propType) { 18 | echo " * @property $propType \$$propName\n"; 19 | } 20 | ?> */ 21 | class <?php echo $className ?> extends <?php echo $daoBaseClass; ?> { 22 | 23 | /** 24 | * Required by older versions of CiviCRM (<5.74). 25 | * @var string 26 | */ 27 | public static $_tableName = <?php var_export($tableName); ?>; 28 | 29 | } 30 | -------------------------------------------------------------------------------- /src/CRM/CivixBundle/Resources/views/Code/form.php.php: -------------------------------------------------------------------------------- 1 | <?php 2 | echo "<?php\n"; 3 | $_namespace = preg_replace(':/:', '_', $namespace); 4 | ?> 5 | 6 | use <?php echo $_namespace ?>_ExtensionUtil as E; 7 | 8 | /** 9 | * Form controller class 10 | * 11 | * @see https://docs.civicrm.org/dev/en/latest/framework/quickform/ 12 | */ 13 | class <?php echo preg_replace(':/:', '_', $fullClassName) ?> extends CRM_Core_Form { 14 | 15 | /** 16 | * @throws \CRM_Core_Exception 17 | */ 18 | public function buildQuickForm(): void { 19 | 20 | // add form elements 21 | $this->add( 22 | 'select', // field type 23 | 'favorite_color', // field name 24 | 'Favorite Color', // field label 25 | $this->getColorOptions(), // list of options 26 | TRUE // is required 27 | ); 28 | $this->addButtons([ 29 | [ 30 | 'type' => 'submit', 31 | 'name' => E::ts('Submit'), 32 | 'isDefault' => TRUE, 33 | ], 34 | ]); 35 | 36 | // export form elements 37 | $this->assign('elementNames', $this->getRenderableElementNames()); 38 | parent::buildQuickForm(); 39 | } 40 | 41 | public function postProcess(): void { 42 | $values = $this->exportValues(); 43 | $options = $this->getColorOptions(); 44 | CRM_Core_Session::setStatus(E::ts('You picked color "%1"', [ 45 | 1 => $options[$values['favorite_color']], 46 | ])); 47 | parent::postProcess(); 48 | } 49 | 50 | public function getColorOptions(): array { 51 | $options = [ 52 | '' => E::ts('- select -'), 53 | '#f00' => E::ts('Red'), 54 | '#0f0' => E::ts('Green'), 55 | '#00f' => E::ts('Blue'), 56 | '#f0f' => E::ts('Purple'), 57 | ]; 58 | foreach (['1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e'] as $f) { 59 | $options["#{$f}{$f}{$f}"] = E::ts('Grey (%1)', [1 => $f]); 60 | } 61 | return $options; 62 | } 63 | 64 | /** 65 | * Get the fields/elements defined in this form. 66 | * 67 | * @return array (string) 68 | */ 69 | public function getRenderableElementNames(): array { 70 | // The _elements list includes some items which should not be 71 | // auto-rendered in the loop -- such as "qfKey" and "buttons". These 72 | // items don't have labels. We'll identify renderable by filtering on 73 | // the 'label'. 74 | $elementNames = []; 75 | foreach ($this->_elements as $element) { 76 | /** @var HTML_QuickForm_Element $element */ 77 | $label = $element->getLabel(); 78 | if (!empty($label)) { 79 | $elementNames[] = $element->getName(); 80 | } 81 | } 82 | return $elementNames; 83 | } 84 | 85 | } 86 | -------------------------------------------------------------------------------- /src/CRM/CivixBundle/Resources/views/Code/form.tpl.php: -------------------------------------------------------------------------------- 1 | {* HEADER *} 2 | 3 | <div class="crm-submit-buttons"> 4 | {include file="CRM/common/formButtons.tpl" location="top"} 5 | </div> 6 | 7 | {* FIELD EXAMPLE: OPTION 1 (AUTOMATIC LAYOUT) *} 8 | 9 | {foreach from=$elementNames item=elementName} 10 | <div class="crm-section"> 11 | <div class="label">{$form.$elementName.label}</div> 12 | <div class="content">{$form.$elementName.html}</div> 13 | <div class="clear"></div> 14 | </div> 15 | {/foreach} 16 | 17 | {* FIELD EXAMPLE: OPTION 2 (MANUAL LAYOUT) 18 | 19 | <div> 20 | <span>{$form.favorite_color.label}</span> 21 | <span>{$form.favorite_color.html}</span> 22 | </div> 23 | 24 | {* FOOTER *} 25 | <div class="crm-submit-buttons"> 26 | {include file="CRM/common/formButtons.tpl" location="bottom"} 27 | </div> 28 | -------------------------------------------------------------------------------- /src/CRM/CivixBundle/Resources/views/Code/module.php.php: -------------------------------------------------------------------------------- 1 | <?php 2 | echo "<?php\n"; 3 | $_namespace = preg_replace(':/:', '_', $namespace); 4 | ?> 5 | 6 | require_once '<?php echo $mainFile ?>.civix.php'; 7 | 8 | use <?php echo $_namespace ?>_ExtensionUtil as E; 9 | 10 | /** 11 | * Implements hook_civicrm_config(). 12 | * 13 | * @link https://docs.civicrm.org/dev/en/latest/hooks/hook_civicrm_config/ 14 | */ 15 | function <?php echo $mainFile ?>_civicrm_config(&$config): void { 16 | _<?php echo $mainFile ?>_civix_civicrm_config($config); 17 | } 18 | 19 | /** 20 | * Implements hook_civicrm_install(). 21 | * 22 | * @link https://docs.civicrm.org/dev/en/latest/hooks/hook_civicrm_install 23 | */ 24 | function <?php echo $mainFile ?>_civicrm_install(): void { 25 | _<?php echo $mainFile ?>_civix_civicrm_install(); 26 | } 27 | 28 | /** 29 | * Implements hook_civicrm_enable(). 30 | * 31 | * @link https://docs.civicrm.org/dev/en/latest/hooks/hook_civicrm_enable 32 | */ 33 | function <?php echo $mainFile ?>_civicrm_enable(): void { 34 | _<?php echo $mainFile ?>_civix_civicrm_enable(); 35 | } 36 | -------------------------------------------------------------------------------- /src/CRM/CivixBundle/Resources/views/Code/page.php.php: -------------------------------------------------------------------------------- 1 | <?php 2 | echo "<?php\n"; 3 | $_namespace = preg_replace(':/:', '_', $namespace); 4 | ?> 5 | use <?php echo $_namespace ?>_ExtensionUtil as E; 6 | 7 | class <?php echo preg_replace(':/:', '_', $fullClassName) ?> extends CRM_Core_Page { 8 | 9 | public function run() { 10 | // Example: Set the page-title dynamically; alternatively, declare a static title in xml/Menu/*.xml 11 | CRM_Utils_System::setTitle(E::ts('<?php echo $shortClassName ?>')); 12 | 13 | // Example: Assign a variable for use in a template 14 | $this->assign('currentTime', date('Y-m-d H:i:s')); 15 | 16 | parent::run(); 17 | } 18 | 19 | } 20 | -------------------------------------------------------------------------------- /src/CRM/CivixBundle/Resources/views/Code/page.tpl.php: -------------------------------------------------------------------------------- 1 | <h3>This new page is generated by <?php echo $namespace ?>/Page/<?php echo $shortClassName ?>.php</h3> 2 | 3 | {* Example: Display a variable directly *} 4 | <p>The current time is {$currentTime}</p> 5 | 6 | {* Example: Display a translated string -- which happens to include a variable *} 7 | <p>{ts 1=$currentTime}(In your native language) The current time is %1.{/ts}</p> 8 | -------------------------------------------------------------------------------- /src/CRM/CivixBundle/Resources/views/Code/phpunit-boot-cv.php.php: -------------------------------------------------------------------------------- 1 | <?php 2 | echo "<?php\n"; 3 | ?> 4 | 5 | ini_set('memory_limit', '2G'); 6 | 7 | // phpcs:disable 8 | eval(cv('php:boot --level=classloader', 'phpcode')); 9 | // phpcs:enable 10 | // Allow autoloading of PHPUnit helper classes in this extension. 11 | $loader = new \Composer\Autoload\ClassLoader(); 12 | $loader->add('CRM_', [__DIR__ . '/../..', __DIR__]); 13 | $loader->addPsr4('Civi\\', [__DIR__ . '/../../Civi', __DIR__ . '/Civi']); 14 | $loader->add('api_', [__DIR__ . '/../..', __DIR__]); 15 | $loader->addPsr4('api\\', [__DIR__ . '/../../api', __DIR__ . '/api']); 16 | 17 | $loader->register(); 18 | 19 | /** 20 | * Call the "cv" command. 21 | * 22 | * @param string $cmd 23 | * The rest of the command to send. 24 | * @param string $decode 25 | * Ex: 'json' or 'phpcode'. 26 | * @return mixed 27 | * Response output (if the command executed normally). 28 | * For 'raw' or 'phpcode', this will be a string. For 'json', it could be any JSON value. 29 | * @throws \RuntimeException 30 | * If the command terminates abnormally. 31 | */ 32 | function cv(string $cmd, string $decode = 'json') { 33 | $cmd = 'cv ' . $cmd; 34 | $descriptorSpec = [0 => ['pipe', 'r'], 1 => ['pipe', 'w'], 2 => STDERR]; 35 | $oldOutput = getenv('CV_OUTPUT'); 36 | putenv('CV_OUTPUT=json'); 37 | 38 | // Execute `cv` in the original folder. This is a work-around for 39 | // phpunit/codeception, which seem to manipulate PWD. 40 | $cmd = sprintf('cd %s; %s', escapeshellarg(getenv('PWD')), $cmd); 41 | 42 | $process = proc_open($cmd, $descriptorSpec, $pipes, __DIR__); 43 | putenv("CV_OUTPUT=$oldOutput"); 44 | fclose($pipes[0]); 45 | $result = stream_get_contents($pipes[1]); 46 | fclose($pipes[1]); 47 | if (proc_close($process) !== 0) { 48 | throw new RuntimeException("Command failed ($cmd):\n$result"); 49 | } 50 | switch ($decode) { 51 | case 'raw': 52 | return $result; 53 | 54 | case 'phpcode': 55 | // If the last output is /*PHPCODE*/, then we managed to complete execution. 56 | if (substr(trim($result), 0, 12) !== '/*BEGINPHP*/' || substr(trim($result), -10) !== '/*ENDPHP*/') { 57 | throw new \RuntimeException("Command failed ($cmd):\n$result"); 58 | } 59 | return $result; 60 | 61 | case 'json': 62 | return json_decode($result, 1); 63 | 64 | default: 65 | throw new RuntimeException("Bad decoder format ($decode)"); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/CRM/CivixBundle/Resources/views/Code/phpunit-boot.php.php: -------------------------------------------------------------------------------- 1 | <?php 2 | echo "<?php\n"; 3 | ?> 4 | 5 | // Note: $b overrides $a 6 | function _civix_phpunit_settings_merge($a, $b) { 7 | $result = $a; 8 | $b = (array) $b; 9 | foreach ($b as $k1 => $v1) { 10 | foreach ($v1 as $k2 => $v2) { 11 | $result[$k1][$k2] = $v2; 12 | } 13 | } 14 | return $result; 15 | } 16 | 17 | /** 18 | * Install extensions on test database 19 | */ 20 | function _civix_phpunit_setUp() { 21 | static $init = FALSE; 22 | if ($init) { 23 | return; 24 | } 25 | $init = TRUE; 26 | 27 | global $civicrm_setting; 28 | //print_r(array('install' => $civicrm_setting['Test']['test_extensions'])); 29 | $apiResult = civicrm_api('Extension', 'install', array( 30 | 'version' => 3, 31 | 'keys' => $civicrm_setting['Test']['test_extensions'], 32 | )); 33 | register_shutdown_function('_civix_phpunit_tearDown'); 34 | if ($apiResult['is_error'] != 0) { 35 | throw new Exception("Failed to pre-install extensions: " . $apiResult['error_message']); 36 | } 37 | } 38 | 39 | /** 40 | * Uninstall extensions on test database 41 | */ 42 | function _civix_phpunit_tearDown() { 43 | global $civicrm_setting; 44 | //print_r(array('disable' => $civicrm_setting['Test']['test_extensions'])); 45 | $result = civicrm_api('Extension', 'disable', array( 46 | 'version' => 3, 47 | 'keys' => $civicrm_setting['Test']['test_extensions'], 48 | )); 49 | //print_r(array('uninstall' => $civicrm_setting['Test']['test_extensions'])); 50 | $result = civicrm_api('Extension', 'uninstall', array( 51 | 'version' => 3, 52 | 'keys' => $civicrm_setting['Test']['test_extensions'], 53 | 'removeFiles' => FALSE, 54 | )); 55 | } 56 | 57 | global $civicrm_setting; 58 | $civix_civicrm_setting = <?php var_export($civicrm_setting); ?>; 59 | $civicrm_setting = _civix_phpunit_settings_merge($civix_civicrm_setting, $civicrm_setting); 60 | -------------------------------------------------------------------------------- /src/CRM/CivixBundle/Resources/views/Code/readme.md.php: -------------------------------------------------------------------------------- 1 | # <?php echo $fullName; ?> 2 | 3 | (*FIXME: In one or two paragraphs, describe what the extension does and why one would download it. *) 4 | 5 | This is an [extension for CiviCRM](https://docs.civicrm.org/sysadmin/en/latest/customize/extensions/), licensed under [<?php echo $license ?>](LICENSE.txt). 6 | 7 | ## Getting Started 8 | 9 | (* FIXME: Where would a new user navigate to get started? What changes would they see? *) 10 | 11 | ## Known Issues 12 | 13 | (* FIXME *) 14 | -------------------------------------------------------------------------------- /src/CRM/CivixBundle/Resources/views/Code/report.tpl.php: -------------------------------------------------------------------------------- 1 | {* Use the default layout *} 2 | {include file="CRM/Report/Form.tpl"} 3 | -------------------------------------------------------------------------------- /src/CRM/CivixBundle/Resources/views/Code/service.php.php: -------------------------------------------------------------------------------- 1 | <?php 2 | echo "<" . "?php\n"; 3 | if ($classNamespaceDecl) { 4 | echo "$classNamespaceDecl\n\n"; 5 | } 6 | echo "$useE\n"; 7 | ?> 8 | use Civi\Core\Service\AutoService; 9 | use Symfony\Component\EventDispatcher\EventSubscriberInterface; 10 | 11 | /** 12 | * @service <?php echo "$service\n"; ?> 13 | */ 14 | class <?php echo $className; ?> extends AutoService implements EventSubscriberInterface { 15 | 16 | // TIP: Many services implement `EventSubscriberInterface`. However, this can be omitted if you don't need it. 17 | 18 | public static function getSubscribedEvents(): array { 19 | return [ 20 | // '&hook_civicrm_alterContent' => ['onAlterContent', 0], 21 | // '&hook_civicrm_postCommit::Contribution' => ['onContribute', 0], 22 | // TIP: For hooks based on GenericHookEvent, the "&" will expand arguments. 23 | ]; 24 | } 25 | 26 | // /** 27 | // * @see \CRM_Utils_Hook::alterContent() 28 | // */ 29 | // public function onAlterContent(&$content, $context, $tplName, &$object) { ... } 30 | 31 | // /** 32 | // * @see \CRM_Utils_Hook::postCommit() 33 | // */ 34 | // public function onContribute($op, $objectName, $objectId, $objectRef = NULL) { ... } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /src/CRM/CivixBundle/Resources/views/Code/test-api.php.php: -------------------------------------------------------------------------------- 1 | <?php 2 | echo "<?php\n"; 3 | ?> 4 | 5 | use Civi\Test\CiviEnvBuilder; 6 | use Civi\Test\HeadlessInterface; 7 | use Civi\Test\HookInterface; 8 | use Civi\Test\TransactionalInterface; 9 | 10 | /** 11 | * <?php echo $entityNameCamel ?>.<?php echo $actionNameCamel ?> API Test Case 12 | * This is a generic test class implemented with PHPUnit. 13 | * @group headless 14 | */ 15 | class <?php echo $testClassName ?> extends \PHPUnit\Framework\TestCase implements HeadlessInterface, HookInterface, TransactionalInterface { 16 | use \Civi\Test\Api3TestTrait; 17 | 18 | /** 19 | * Set up for headless tests. 20 | * 21 | * Civi\Test has many helpers, like install(), uninstall(), sql(), and sqlFile(). 22 | * 23 | * See: https://docs.civicrm.org/dev/en/latest/testing/phpunit/#civitest 24 | */ 25 | public function setUpHeadless(): CiviEnvBuilder { 26 | return \Civi\Test::headless() 27 | ->installMe(__DIR__) 28 | ->apply(); 29 | } 30 | 31 | /** 32 | * The setup() method is executed before the test is executed (optional). 33 | */ 34 | public function setUp(): void { 35 | parent::setUp(); 36 | } 37 | 38 | /** 39 | * The tearDown() method is executed after the test was executed (optional) 40 | * This can be used for cleanup. 41 | */ 42 | public function tearDown(): void { 43 | parent::tearDown(); 44 | } 45 | 46 | /** 47 | * Simple example test case. 48 | * 49 | * Note how the function name begins with the word "test". 50 | */ 51 | public function testApiExample() { 52 | $result = civicrm_api3('<?php echo $entityNameCamel ?>', '<?php echo $actionNameLower ?>', array('magicword' => 'sesame')); 53 | $this->assertEquals('Twelve', $result['values'][12]['name']); 54 | } 55 | 56 | } 57 | -------------------------------------------------------------------------------- /src/CRM/CivixBundle/Resources/views/Code/test-e2e.php.php: -------------------------------------------------------------------------------- 1 | <?php 2 | echo "<?php\n"; 3 | 4 | if ($testNamespace) { 5 | echo "namespace $testNamespace;\n"; 6 | } 7 | $_namespace = preg_replace(':/:', '_', $namespace); 8 | ?> 9 | 10 | use <?php echo $_namespace ?>_ExtensionUtil as E; 11 | use Civi\Test\EndToEndInterface; 12 | 13 | /** 14 | * FIXME - Add test description. 15 | * 16 | * Tips: 17 | * - The global variable $_CV has some properties which may be useful, such as: 18 | * CMS_URL, ADMIN_USER, ADMIN_PASS, ADMIN_EMAIL, DEMO_USER, DEMO_PASS, DEMO_EMAIL. 19 | * - To spawn a new CiviCRM thread and execute an API call or PHP code, use cv(), e.g. 20 | * cv('api system.flush'); 21 | * $data = cv('eval "return Civi::settings()->get(\'foobar\')"'); 22 | * $dashboardUrl = cv('url civicrm/dashboard'); 23 | * - This template uses the most generic base-class, but you may want to use a more 24 | * powerful base class, such as \PHPUnit_Extensions_SeleniumTestCase or 25 | * \PHPUnit_Extensions_Selenium2TestCase. 26 | * See also: https://phpunit.de/manual/4.8/en/selenium.html 27 | * 28 | * @group e2e 29 | * @see cv 30 | */ 31 | class <?php echo $testClass ?> extends \PHPUnit\Framework\TestCase implements EndToEndInterface { 32 | 33 | public static function setUpBeforeClass(): void { 34 | // See: https://docs.civicrm.org/dev/en/latest/testing/phpunit/#civitest 35 | 36 | // Example: Install this extension. Don't care about anything else. 37 | \Civi\Test::e2e()->installMe(__DIR__)->apply(); 38 | 39 | // Example: Uninstall all extensions except this one. 40 | // \Civi\Test::e2e()->uninstall('*')->installMe(__DIR__)->apply(); 41 | 42 | // Example: Install only core civicrm extensions. 43 | // \Civi\Test::e2e()->uninstall('*')->install('org.civicrm.*')->apply(); 44 | } 45 | 46 | public function setUp(): void { 47 | parent::setUp(); 48 | } 49 | 50 | public function tearDown(): void { 51 | parent::tearDown(); 52 | } 53 | 54 | /** 55 | * Example: Test that a version is returned. 56 | */ 57 | public function testWellFormedVersion(): void { 58 | $this->assertNotEmpty(E::SHORT_NAME); 59 | $this->assertMatchesRegularExpression('/^([0-9\.]|alpha|beta)*$/', \CRM_Utils_System::version()); 60 | } 61 | 62 | /** 63 | * Example: Test that we're using a real CMS (Drupal, WordPress, etc). 64 | */ 65 | public function testWellFormedUF(): void { 66 | $this->assertMatchesRegularExpression('/^(Drupal|Backdrop|WordPress|Joomla)/', CIVICRM_UF); 67 | } 68 | 69 | } 70 | -------------------------------------------------------------------------------- /src/CRM/CivixBundle/Resources/views/Code/test-headless.php.php: -------------------------------------------------------------------------------- 1 | <?php 2 | echo "<?php\n"; 3 | 4 | if ($testNamespace) { 5 | echo "namespace $testNamespace;\n"; 6 | } 7 | $_namespace = preg_replace(':/:', '_', $namespace); 8 | ?> 9 | 10 | use <?php echo $_namespace ?>_ExtensionUtil as E; 11 | use Civi\Test\CiviEnvBuilder; 12 | use Civi\Test\HeadlessInterface; 13 | use Civi\Test\HookInterface; 14 | use Civi\Test\TransactionalInterface; 15 | 16 | /** 17 | * FIXME - Add test description. 18 | * 19 | * Tips: 20 | * - With HookInterface, you may implement CiviCRM hooks directly in the test class. 21 | * Simply create corresponding functions (e.g. "hook_civicrm_post(...)" or similar). 22 | * - With TransactionalInterface, any data changes made by setUp() or test****() functions will 23 | * rollback automatically -- as long as you don't manipulate schema or truncate tables. 24 | * If this test needs to manipulate schema or truncate tables, then either: 25 | * a. Do all that using setupHeadless() and Civi\Test. 26 | * b. Disable TransactionalInterface, and handle all setup/teardown yourself. 27 | * 28 | * @group headless 29 | */ 30 | class <?php echo $testClass ?> extends \PHPUnit\Framework\TestCase implements HeadlessInterface, HookInterface, TransactionalInterface { 31 | 32 | /** 33 | * Setup used when HeadlessInterface is implemented. 34 | * 35 | * Civi\Test has many helpers, like install(), uninstall(), sql(), and sqlFile(). 36 | * 37 | * @link https://github.com/civicrm/org.civicrm.testapalooza/blob/master/civi-test.md 38 | * 39 | * @return \Civi\Test\CiviEnvBuilder 40 | * 41 | * @throws \CRM_Extension_Exception_ParseException 42 | */ 43 | public function setUpHeadless(): CiviEnvBuilder { 44 | return \Civi\Test::headless() 45 | ->installMe(__DIR__) 46 | ->apply(); 47 | } 48 | 49 | public function setUp():void { 50 | parent::setUp(); 51 | } 52 | 53 | public function tearDown():void { 54 | parent::tearDown(); 55 | } 56 | 57 | /** 58 | * Example: Test that a version is returned. 59 | */ 60 | public function testWellFormedVersion():void { 61 | $this->assertNotEmpty(E::SHORT_NAME); 62 | $this->assertMatchesRegularExpression('/^([0-9\.]|alpha|beta)*$/', \CRM_Utils_System::version()); 63 | } 64 | 65 | /** 66 | * Example: Test that we're using a fake CMS. 67 | */ 68 | public function testWellFormedUF():void { 69 | $this->assertEquals('UnitTests', CIVICRM_UF); 70 | } 71 | 72 | } 73 | -------------------------------------------------------------------------------- /src/CRM/CivixBundle/Resources/views/Code/test-legacy.php.php: -------------------------------------------------------------------------------- 1 | <?php 2 | echo "<?php\n"; 3 | 4 | if ($testNamespace) { 5 | echo "namespace $testNamespace;\n"; 6 | } 7 | $_namespace = preg_replace(':/:', '_', $namespace); 8 | ?> 9 | 10 | use <?php echo $_namespace ?>_ExtensionUtil as E; 11 | use Civi\Test\HeadlessInterface; 12 | 13 | /** 14 | * FIXME - Add test description. 15 | * 16 | * Note: Legacy test cases are based on CiviUnitTestCase. These should work about 17 | * as well (or as poorly) as before. This generator is provided primarily as a way 18 | * test backward compatibility. 19 | * 20 | * @group headless 21 | * @group legacy 22 | */ 23 | class <?php echo $testClass ?> extends \CiviUnitTestCase implements HeadlessInterface { 24 | 25 | /** 26 | * Setup for when Headless interface is implemented. 27 | * 28 | * `CiviTestListener`+`HeadlessInterface` has some clever tricks for 29 | * bootstrapping which make it easier to use phpunit CLI. 30 | * However, there's not much point in using setupHeadless() with CiviUnitTestCase 31 | * because CiviUnitTestCase performs its own special setup/teardown logic. 32 | */ 33 | public function setUpHeadless(): void {} 34 | 35 | /** 36 | * Setup any fixtures required for the tests in this class. 37 | */ 38 | public function setUp(): void { 39 | parent::setUp(); 40 | } 41 | 42 | /** 43 | * Return the database to the original state.. 44 | */ 45 | public function tearDown(): void { 46 | parent::tearDown(); 47 | } 48 | 49 | /** 50 | * Example: Test that a version is returned. 51 | */ 52 | public function testWellFormedVersion(): void { 53 | $this->assertNotEmpty(E::SHORT_NAME); 54 | $this->assertMatchesRegularExpression('/^([0-9\.]|alpha|beta)*$/', \CRM_Utils_System::version()); 55 | } 56 | 57 | /** 58 | * Example: Test that we're using a fake CMS. 59 | */ 60 | public function testWellFormedUF(): void { 61 | $this->assertEquals('UnitTests', CIVICRM_UF); 62 | } 63 | 64 | } 65 | -------------------------------------------------------------------------------- /src/CRM/CivixBundle/Resources/views/Code/test-phpunit.php.php: -------------------------------------------------------------------------------- 1 | <?php 2 | echo "<?php\n"; 3 | ?> 4 | 5 | /** 6 | * This is a generic test class for the extension (implemented with PHPUnit). 7 | */ 8 | class <?php echo $testClass ?> extends \PHPUnit\Framework\TestCase { 9 | 10 | /** 11 | * The setup() method is executed before the test is executed (optional). 12 | */ 13 | public function setUp():void { 14 | parent::setUp(); 15 | } 16 | 17 | /** 18 | * The tearDown() method is executed after the test was executed (optional). 19 | * 20 | * This can be used for cleanup. 21 | */ 22 | public function tearDown():void { 23 | parent::tearDown(); 24 | } 25 | 26 | /** 27 | * Simple example test case. 28 | * 29 | * Note how the function name begins with the word "test". 30 | */ 31 | public function testExample():void { 32 | self::assertTrue(TRUE, "The argument must be true to pass the test"); 33 | } 34 | 35 | } 36 | -------------------------------------------------------------------------------- /src/CRM/CivixBundle/RunMethodsTrait.php: -------------------------------------------------------------------------------- 1 | <?php 2 | 3 | namespace CRM\CivixBundle; 4 | 5 | /** 6 | * Use this trait if you want to delegate to an open-ended list of methods. 7 | * 8 | * For example, suppose you want a method "checkRequirements()" which is built on running 9 | * methods "checkRequirements_foo()","checkRequirements_bar()", etc. 10 | * 11 | * You could implement as: 12 | * 13 | * use RunMethodsTrait; 14 | * public function checkRequirements() { 15 | * $this->runMethods('/^checkRequirements_/'); 16 | * } 17 | * protected function checkRequirements_foo() { ... } 18 | * protected function checkRequirements_bar() { ... } 19 | */ 20 | trait RunMethodsTrait { 21 | 22 | /** 23 | * 24 | * Ex: [$results] = $this->runMethods(';foo.*;'); 25 | * Ex: [$results, $skipped] = $this->runMethods(';foo.*;', ['val1', 'val2']); 26 | * 27 | * @param string $methodRegex 28 | * @param array $params 29 | * A list of parameters to pass down to each method. 30 | * @return array 31 | * Results of calling the methods. 32 | * Some methods have opted-out of execution. 33 | * Formally, the results are a tuple of `[$executed, $skipped]`. 34 | * @throws \ReflectionException 35 | */ 36 | protected function runMethods(string $methodRegex, array $params = []): array { 37 | $executed = []; 38 | $skipped = []; 39 | 40 | $class = new \ReflectionClass($this); 41 | foreach ($class->getMethods() as $method) { 42 | /** @var \ReflectionMethod $method */ 43 | 44 | if (preg_match($methodRegex, $method->getName())) { 45 | try { 46 | $result = $method->invoke($this, ...$params); 47 | $executed[$method->getName()] = $result; 48 | } 49 | catch (SkippedMethodException $e) { 50 | $skipped[$method->getName()] = $method->getName(); 51 | } 52 | } 53 | } 54 | 55 | return [$executed, $skipped]; 56 | } 57 | 58 | /** 59 | * Assert that the current method is (or is not) applicable. 60 | * 61 | * @param bool $bool 62 | * If TRUE, then proceed with normal execution. 63 | * 64 | * If FALSE, raise an exception that will propagate back to the main `testSnapshot()` method. 65 | * The next check will run. 66 | */ 67 | protected function runsIf(bool $bool) { 68 | if (!$bool) { 69 | throw new SkippedMethodException(); 70 | } 71 | } 72 | 73 | } 74 | -------------------------------------------------------------------------------- /src/CRM/CivixBundle/SkippedMethodException.php: -------------------------------------------------------------------------------- 1 | <?php 2 | 3 | namespace CRM\CivixBundle; 4 | 5 | class SkippedMethodException extends \RuntimeException { 6 | } 7 | -------------------------------------------------------------------------------- /src/CRM/CivixBundle/Test/CommandTester.php: -------------------------------------------------------------------------------- 1 | <?php 2 | 3 | namespace CRM\CivixBundle\Test; 4 | 5 | interface CommandTester { 6 | 7 | /** 8 | * Executes the command. 9 | * 10 | * Available execution options: 11 | * 12 | * * interactive: Sets the input interactive flag 13 | * * decorated: Sets the output decorated flag 14 | * * verbosity: Sets the output verbosity flag 15 | * * capture_stderr_separately: Make output of stdOut and stdErr separately available 16 | * 17 | * @param array $input An array of command arguments and options 18 | * @param array $options An array of execution options 19 | * 20 | * @return int The command exit code 21 | */ 22 | public function execute(array $input, array $options = []); 23 | 24 | /** 25 | * @return string 26 | */ 27 | public function getDisplay(bool $normalize = FALSE); 28 | 29 | /** 30 | * @return int 31 | */ 32 | public function getStatusCode(); 33 | 34 | } 35 | -------------------------------------------------------------------------------- /src/CRM/CivixBundle/Test/SubProcessCommandTester.php: -------------------------------------------------------------------------------- 1 | <?php 2 | 3 | namespace CRM\CivixBundle\Test; 4 | 5 | use Symfony\Component\Process\Process; 6 | 7 | class SubProcessCommandTester implements CommandTester { 8 | 9 | /** 10 | * @var array 11 | */ 12 | protected $baseCommand; 13 | 14 | /** 15 | * @var string|null 16 | */ 17 | protected $display; 18 | 19 | /** 20 | * @var int|null 21 | */ 22 | protected $statusCode; 23 | 24 | /** 25 | * @var string|null 26 | */ 27 | protected $commandLine; 28 | 29 | /** 30 | * @param array $baseCommand 31 | */ 32 | public function __construct(array $baseCommand) { 33 | $this->baseCommand = $baseCommand; 34 | } 35 | 36 | /** 37 | * Executes the command. 38 | * 39 | * @param array $input An array of command arguments and options 40 | * @param array $options An array of execution options 41 | * Ignored 42 | * @return int The command exit code 43 | */ 44 | public function execute(array $input, array $options = []) { 45 | if (!empty($options)) { 46 | throw new \LogicException(__CLASS__ . " does not implement support for execute() options"); 47 | } 48 | 49 | $command = $this->baseCommand; 50 | foreach ($input as $key => $value) { 51 | if (substr($key, 0, 2) === '--') { 52 | if ($value === TRUE) { 53 | $command[] = $key; 54 | } 55 | else { 56 | $command[] = "$key=$value"; 57 | } 58 | } 59 | else { 60 | $command[] = $value; 61 | } 62 | } 63 | 64 | $buffer = fopen('php://memory', 'w+'); 65 | 66 | $p = new Process($command); 67 | $this->commandLine = $p->getCommandLine(); 68 | 69 | $p->run(function ($type, $data) use ($buffer) { 70 | // Default policy - combine STDOUT and STDIN into one continuous stream. 71 | fwrite($buffer, $data); 72 | }); 73 | $this->statusCode = $p->getExitCode(); 74 | 75 | rewind($buffer); 76 | $this->display = stream_get_contents($buffer); 77 | fclose($buffer); 78 | 79 | return $this->statusCode; 80 | } 81 | 82 | public function getDisplay(bool $normalize = FALSE) { 83 | if ($normalize) { 84 | return str_replace(\PHP_EOL, "\n", $this->display); 85 | } 86 | else { 87 | return $this->display; 88 | } 89 | } 90 | 91 | public function getStatusCode(): int { 92 | return $this->statusCode; 93 | } 94 | 95 | public function getCommandLine(): string { 96 | return $this->commandLine; 97 | } 98 | 99 | } 100 | -------------------------------------------------------------------------------- /src/CRM/CivixBundle/UpgradeList.php: -------------------------------------------------------------------------------- 1 | <?php 2 | namespace CRM\CivixBundle; 3 | 4 | class UpgradeList { 5 | 6 | /** 7 | * @var array|null 8 | */ 9 | protected $upgrades = NULL; 10 | 11 | /** 12 | * Get the highest known version. 13 | * 14 | * @return string 15 | */ 16 | public function getHeadVersion(): string { 17 | return array_key_last($this->getUpgrades()); 18 | } 19 | 20 | /** 21 | * @return array 22 | * array(string $version => string $filePath) 23 | */ 24 | public function getUpgrades(): array { 25 | if ($this->upgrades === NULL) { 26 | $this->upgrades = $this->scan(); 27 | } 28 | return $this->upgrades; 29 | } 30 | 31 | /** 32 | * Get a list of upgrades appropriate to a particular codebase. 33 | * 34 | * @param string $startVersion 35 | * The initial/start version of the codebase. 36 | * @return array 37 | * array(string $version => string $filePath) 38 | */ 39 | public function findUpgrades(string $startVersion): array { 40 | return array_filter($this->getUpgrades(), 41 | function($upgradeVersion) use ($startVersion) { 42 | return (bool) version_compare($upgradeVersion, $startVersion, '>'); 43 | }, 44 | ARRAY_FILTER_USE_KEY 45 | ); 46 | } 47 | 48 | /** 49 | * Scan for upgrade files. 50 | * 51 | * @return array 52 | * array(string $version => string $filePath) 53 | */ 54 | protected function scan(): array { 55 | $parseVer = function($file) { 56 | return basename($file, '.up.php'); 57 | }; 58 | 59 | $upgrades = []; 60 | $iter = new \DirectoryIterator(Application::findCivixDir() . '/upgrades'); 61 | foreach ($iter as $file) { 62 | /** @var \SplFileInfo $file */ 63 | if (preg_match(';\.up\.php$;', $file->getBasename())) { 64 | $upgrades[$parseVer($file->getBasename())] = $file->getPathname(); 65 | } 66 | } 67 | 68 | uksort($upgrades, 'version_compare'); 69 | return $upgrades; 70 | } 71 | 72 | } 73 | -------------------------------------------------------------------------------- /src/CRM/CivixBundle/Utils/AutoCleanup.php: -------------------------------------------------------------------------------- 1 | <?php 2 | 3 | namespace CRM\CivixBundle\Utils; 4 | 5 | /** 6 | * Define a local cleanup object (which will run on-destruct). 7 | * 8 | * Example: 9 | * 10 | * ``` 11 | * $foo = allocateResource(); 12 | * $cleanup = new AutoCleanup(function() use ($foo) { 13 | * releaseResource($foo); 14 | * }); 15 | * ``` 16 | */ 17 | class AutoCleanup { 18 | 19 | protected $callback; 20 | 21 | /** 22 | * @param $callback 23 | */ 24 | public function __construct($callback) { 25 | $this->callback = $callback; 26 | } 27 | 28 | public function __destruct() { 29 | call_user_func($this->callback); 30 | } 31 | 32 | /** 33 | * Prohibit (de)serialization of AutoCleanup. 34 | * 35 | * The generic nature of AutoClean makes it a potential target for escalating 36 | * serialization vulnerabilities, and there's no good reason for serializing it. 37 | */ 38 | public function __sleep() { 39 | throw new \RuntimeException("AutoCleanup is a runtime helper. It is not intended for serialization."); 40 | } 41 | 42 | /** 43 | * Prohibit (de)serialization of AutoCleanup. 44 | * 45 | * The generic nature of AutoClean makes it a potential target for escalating 46 | * serialization vulnerabilities, and there's no good reason for deserializing it. 47 | */ 48 | public function __wakeup() { 49 | throw new \RuntimeException("AutoCleanup is a runtime helper. It is not intended for deserialization."); 50 | } 51 | 52 | } 53 | -------------------------------------------------------------------------------- /src/CRM/CivixBundle/Utils/CivixStyle.php: -------------------------------------------------------------------------------- 1 | <?php 2 | 3 | namespace CRM\CivixBundle\Utils; 4 | 5 | use Symfony\Component\Console\Style\SymfonyStyle; 6 | 7 | class CivixStyle extends SymfonyStyle { 8 | 9 | } 10 | -------------------------------------------------------------------------------- /src/CRM/CivixBundle/Utils/Commands.php: -------------------------------------------------------------------------------- 1 | <?php 2 | namespace CRM\CivixBundle\Utils; 3 | 4 | use Symfony\Component\Console\Input\InputArgument; 5 | use Symfony\Component\Console\Input\InputInterface; 6 | use Symfony\Component\Console\Input\InputOption; 7 | use Symfony\Component\Console\Output\OutputInterface; 8 | use Symfony\Component\Process\PhpExecutableFinder; 9 | use Symfony\Component\Process\Process; 10 | 11 | /** 12 | * Helper for running Symfony commands 13 | */ 14 | class Commands { 15 | 16 | /** 17 | * Create a new process which executes a different commands 18 | * 19 | * @param string $cmd 20 | * @return \Symfony\Component\Process\Process 21 | */ 22 | public static function createProcess($cmd, $timeout = 300) { 23 | $process = new Process(self::createShellCommand($cmd), NULL, NULL, NULL, $timeout); 24 | return $process; 25 | } 26 | 27 | /** 28 | * Generate the shell statement for invoking of the Symfony commands 29 | * 30 | * @param $cmd symfony command 31 | * @return string a valid shell command 32 | */ 33 | public static function createShellCommand($cmd) { 34 | $php = escapeshellarg(self::getPhp()); 35 | $console = escapeshellarg($_SERVER['PHP_SELF']); 36 | return $php . ' ' . $console . ' ' . $cmd; 37 | } 38 | 39 | protected static function getPhp() { 40 | $phpFinder = new PhpExecutableFinder(); 41 | if (!$phpPath = $phpFinder->find()) { 42 | throw new \RuntimeException('The php executable could not be found, add it to your PATH environment variable and try again'); 43 | } 44 | return $phpPath; 45 | } 46 | 47 | /** 48 | * Determine the full path to an executable 49 | * 50 | * @param $name 51 | * @return string|FALSE 52 | */ 53 | public static function findExecutable($name) { 54 | $paths = explode(PATH_SEPARATOR, getenv('PATH')); 55 | foreach ($paths as $path) { 56 | if (file_exists("$path/$name")) { 57 | return "$path/$name"; 58 | } 59 | } 60 | return FALSE; 61 | } 62 | 63 | } 64 | -------------------------------------------------------------------------------- /src/CRM/CivixBundle/Utils/EvilEx.php: -------------------------------------------------------------------------------- 1 | <?php 2 | 3 | namespace CRM\CivixBundle\Utils; 4 | 5 | /** 6 | * Evil Expressions: What happens when you don't import nikic/php-parser. 7 | */ 8 | class EvilEx { 9 | 10 | /** 11 | * Find a function-body, and run it through a filter. 12 | * 13 | * Limitation: This only matches top-level functions, where the `function x()` begins in the leftmost, 14 | * and where the closing `}` is als in the leftmost, and where the body is indented. 15 | * 16 | * @param string $body 17 | * Full-text file. 18 | * @param string $function 19 | * Name of the function to filter. 20 | * @param callable $filter 21 | * Filter the function-body; return new body. 22 | * function(string $body): string 23 | * @return string 24 | * Updated body. 25 | */ 26 | public static function rewriteFunction(string $body, string $function, callable $filter): string { 27 | $pattern = "/(\nfunction " . $function . "\([^{]+\)\s*{\n)((\n| [^\n]*\n)*)(}\n)/m"; 28 | return preg_replace_callback($pattern, 29 | function ($m) use ($filter) { 30 | $body = $m[2]; 31 | $newBody = $filter($body); 32 | return $m[1] . $newBody . $m[4]; 33 | }, 34 | $body 35 | ); 36 | } 37 | 38 | /** 39 | * Searches for a chunk of code - and replaces it. 40 | * 41 | * When matching the chunk of code, it specifically ignores whitespace and blank lines. 42 | * 43 | * @param string $body 44 | * @param array $matchChunk 45 | * A series of lines which should be found somewhere inside $body (modulo whitespace). 46 | * @param callable $filterChunk 47 | * Filter the matching lines; return new lines. 48 | * function(array $matchLines): array 49 | * @return string 50 | * Updated body. 51 | */ 52 | public static function rewriteMultilineChunk(string $body, array $matchChunk, callable $filterChunk) { 53 | $expectLines = EvilEx::digestLines($matchChunk); 54 | $actualLines = EvilEx::digestLines(explode("\n", $body)); 55 | foreach (array_keys($actualLines) as $startOffset) { 56 | $endOffset = NULL; 57 | $expectLineNum = 0; 58 | for ($actualLineNum = $startOffset; $actualLineNum < count($actualLines); $actualLineNum++) { 59 | if (empty($actualLines[$actualLineNum]['dig'])) { 60 | continue; 61 | } 62 | if ($expectLines[$expectLineNum]['dig'] !== $actualLines[$actualLineNum]['dig']) { 63 | continue 2; 64 | } 65 | $expectLineNum++; 66 | if ($expectLineNum >= count($expectLines)) { 67 | $endOffset = $actualLineNum; 68 | break 2; 69 | } 70 | } 71 | } 72 | 73 | if ($endOffset === NULL) { 74 | return $body; 75 | } 76 | 77 | $rawActualLines = array_column($actualLines, 'raw'); 78 | $matchLines = array_slice($rawActualLines, $startOffset, $endOffset - $startOffset + 1, TRUE); 79 | $newLines = $filterChunk($matchLines); 80 | array_splice($rawActualLines, $startOffset, $endOffset - $startOffset + 1, $newLines); 81 | return implode("\n", $rawActualLines); 82 | } 83 | 84 | public static function digestLine(string $line): string { 85 | return mb_strtolower(preg_replace('/\s+/', '', $line)); 86 | } 87 | 88 | public static function digestLines($lines): array { 89 | $result = []; 90 | foreach ($lines as $line) { 91 | $result[] = ['raw' => $line, 'dig' => static::digestLine($line)]; 92 | } 93 | return $result; 94 | } 95 | 96 | } 97 | -------------------------------------------------------------------------------- /src/CRM/CivixBundle/Utils/Formatting.php: -------------------------------------------------------------------------------- 1 | <?php 2 | 3 | namespace CRM\CivixBundle\Utils; 4 | 5 | class Formatting { 6 | 7 | /** 8 | * Format an ordered list. 9 | * 10 | * @param string $itemFormat 11 | * Ex: "Hello %s.\n" 12 | * Ex: "Hello %s from %s.\n" 13 | * Ex: "Everyone in %2$s says hello to %1$s." 14 | * @param iterable $list 15 | * Ex: ['Alice', 'Bob'] 16 | * Ex: [['Alice','Argentina'], ['Bob','Britain']] 17 | * @return string 18 | * Ex: "1. Hello Alice.\n2. Hello Bob.\n" 19 | * Ex: "1. Hello Alice from Argentina!\n2. Hello Bob from Britain!" 20 | */ 21 | public static function ol(string $itemFormat, iterable $list) { 22 | $buf = ''; 23 | foreach ($list as $int => $item) { 24 | $item = (array) $item; 25 | $line = sprintf($itemFormat, ...$item); 26 | $buf .= ((1 + $int) . ". " . $line); 27 | } 28 | return rtrim($buf); 29 | } 30 | 31 | /** 32 | * Format an unordered list. 33 | * 34 | * @param string $itemFormat 35 | * Ex: "Hello %s.\n" 36 | * Ex: "Hello %s from %s.\n" 37 | * Ex: "Everyone in %2$s says hello to %1$s." 38 | * @param iterable $list 39 | * Ex: ['Alice', 'Bob'] 40 | * Ex: [['Alice','Argentina'], ['Bob','Britain']] 41 | * @return string 42 | * Ex: "Hello Alice.\nHello Bob.\n" 43 | * Ex: "Hello alice from Argentina!\nHello Bob from Britain!" 44 | */ 45 | public static function ul(string $itemFormat, iterable $list) { 46 | $buf = ''; 47 | foreach ($list as $int => $item) { 48 | $item = (array) $item; 49 | $line = sprintf($itemFormat, ...$item); 50 | $buf .= "- " . $line; 51 | } 52 | return rtrim($buf); 53 | } 54 | 55 | /** 56 | * @param array $headers 57 | * @param array $rows 58 | * @return string 59 | */ 60 | public static function table(array $headers, array $rows) { 61 | $buffer = new \Symfony\Component\Console\Output\BufferedOutput(); 62 | $table = new \Symfony\Component\Console\Helper\Table($buffer); 63 | $table->setHeaders($headers); 64 | $table->setRows($rows); 65 | $table->render(); 66 | return $buffer->fetch(); 67 | } 68 | 69 | } 70 | -------------------------------------------------------------------------------- /src/CRM/CivixBundle/Utils/Functions.php: -------------------------------------------------------------------------------- 1 | <?php 2 | 3 | namespace CRM\CivixBundle\Utils; 4 | 5 | class Functions { 6 | 7 | /** 8 | * Create a "curried function" in which some arguments are pre-applied. 9 | * 10 | * Ex: 11 | * $sha256 = Functions::curry('hash', 'sha256'); 12 | * echo $sha256('hello world'); 13 | * // Equivalent to `hash('sha256', 'hello world')` 14 | * 15 | * @param callable $callable 16 | * @param mixed ...$args1 17 | * @return \Closure 18 | */ 19 | public static function curry($callable, ...$args1) { 20 | return function (...$args2) use ($callable, $args1) { 21 | $allArgs = array_merge($args1, $args2); 22 | return $callable(...$allArgs); 23 | }; 24 | } 25 | 26 | } 27 | -------------------------------------------------------------------------------- /src/CRM/CivixBundle/Utils/IOStack.php: -------------------------------------------------------------------------------- 1 | <?php 2 | 3 | namespace CRM\CivixBundle\Utils; 4 | 5 | class IOStack { 6 | 7 | protected $stack = []; 8 | 9 | public function push(\Symfony\Component\Console\Input\InputInterface $input, \Symfony\Component\Console\Output\OutputInterface $output): void { 10 | array_unshift($this->stack, [ 11 | 'input' => $input, 12 | 'output' => $output, 13 | 'io' => new CivixStyle($input, $output), 14 | ]); 15 | } 16 | 17 | public function pop(): array { 18 | return array_shift($this->stack); 19 | } 20 | 21 | public function current(string $property) { 22 | return $this->stack[0][$property]; 23 | } 24 | 25 | public function reset() { 26 | $this->stack = []; 27 | } 28 | 29 | } 30 | -------------------------------------------------------------------------------- /src/CRM/CivixBundle/Utils/Path.php: -------------------------------------------------------------------------------- 1 | <?php 2 | 3 | namespace CRM\CivixBundle\Utils; 4 | 5 | use Webmozart\Glob\Glob; 6 | 7 | class Path { 8 | 9 | /** 10 | * @var string 11 | */ 12 | protected $basedir; 13 | 14 | /** 15 | * @param string|Path $basedir 16 | * @param string[] $args 17 | * Optionally append to the path 18 | * @return Path 19 | */ 20 | public static function for($basedir, ...$args): Path { 21 | $result = new Path($basedir); 22 | if (!empty($args)) { 23 | $result = $result->path(...$args); 24 | } 25 | return $result; 26 | } 27 | 28 | /** 29 | * @param string|Path $basedir 30 | */ 31 | public function __construct($basedir) { 32 | if ($basedir instanceof Path) { 33 | $this->basedir = $basedir->basedir; 34 | } 35 | else { 36 | $this->basedir = static::normalize($basedir); 37 | } 38 | } 39 | 40 | public function __toString(): string { 41 | return $this->basedir; 42 | } 43 | 44 | /** 45 | * Determine the full path to a file underneath this path 46 | * 47 | * ex: $basepath = $path->string() 48 | * ex: $item = $this->string('subdir', 'file.xml'); 49 | * 50 | * @return string 51 | */ 52 | public function string() { 53 | $args = func_get_args(); 54 | array_unshift($args, $this->basedir); 55 | return static::normalize(implode('/', $args)); 56 | } 57 | 58 | /** 59 | * Construct the full path to a file underneath this path 60 | * 61 | * ex: $item = $this->path('subdir', 'file.xml'); 62 | * 63 | * @return Path 64 | */ 65 | public function path() { 66 | $args = func_get_args(); 67 | array_unshift($args, $this->basedir); 68 | return new Path(implode('/', $args)); 69 | } 70 | 71 | /** 72 | * Make a folder for this path (if necessary). 73 | * 74 | * @param int $mode 75 | */ 76 | public function mkdir($mode = 0777): void { 77 | $args = func_get_args(); 78 | $dir = call_user_func_array([$this, 'string'], $args); 79 | if (!is_dir($dir)) { 80 | if (!mkdir($dir, $mode, TRUE)) { 81 | throw new \RuntimeException("Failed to make directory: $path"); 82 | } 83 | } 84 | } 85 | 86 | /** 87 | * Recursively search for files matching $pattern. 88 | * 89 | * @param string $pattern 90 | * Ex: 'glob:foobar/*.xml' (non-recursive search) 91 | * Ex: 'find:*.whizbang.php' (recursive search, under .) 92 | * Ex: 'find:meta/*.whizbang.php' (recursive search, under ./meta) 93 | * @return array 94 | */ 95 | public function search($pattern): array { 96 | [$patternType, $patternValue] = explode(':', $pattern, 2); 97 | switch ($patternType) { 98 | case 'glob': 99 | return Glob::glob($this->string($patternValue)); 100 | 101 | case 'find': 102 | $relDir = dirname($patternValue); 103 | $filePat = basename($patternValue); 104 | return Files::findFiles($this->string($relDir), $filePat); 105 | 106 | default: 107 | throw new \RuntimeException("Unrecognized file pattern: $pattern"); 108 | } 109 | } 110 | 111 | protected static function normalize(string $path) { 112 | return str_replace('\\', '/', $path); 113 | } 114 | 115 | } 116 | -------------------------------------------------------------------------------- /src/CRM/CivixBundle/Utils/PathTest.php: -------------------------------------------------------------------------------- 1 | <?php 2 | 3 | namespace CRM\CivixBundle\Utils; 4 | 5 | /** 6 | * @group unit 7 | */ 8 | class PathTest extends \PHPUnit\Framework\TestCase { 9 | 10 | public function testPathFor() { 11 | $this->assertEquals('/var/www', Path::for('/var/www')); 12 | $this->assertEquals('e:/project/web', Path::for('e:\\project\\web')); 13 | $this->assertEquals('e:/project/web/ab/cd/ef', Path::for('e:\\project\\web', 'ab', 'cd\\ef')); 14 | $this->assertEquals('/var/www/foo/bar', Path::for('/var/www', 'foo/bar')); 15 | $this->assertEquals('/var/www/foo/bar', Path::for('/var/www', 'foo\bar')); 16 | $this->assertEquals('/var/www/foobar/whizbang', Path::for('/var/www', 'foobar', 'whizbang')); 17 | } 18 | 19 | public function testPathString() { 20 | $base = Path::for('/var/www'); 21 | $this->assertEquals('/var/www', $base->string()); 22 | $this->assertEquals('/var/www/foo', $base->string('foo')); 23 | $this->assertEquals('/var/www/foo/bar', $base->string('foo', 'bar')); 24 | $this->assertEquals('/var/www/foo/bar', $base->string('foo/bar')); 25 | $this->assertEquals('/var/www/foo/bar', $base->string('foo\\bar')); 26 | } 27 | 28 | public function testPathChild() { 29 | $this->assertEquals('/var/www/foo', (string) Path::for('/var')->path('www')->path('foo')); 30 | $this->assertEquals('/var/www/foo/whiz/bang', (string) Path::for('/var')->path('www', 'foo')->path('whiz/bang')); 31 | $this->assertEquals('e:/work/www/foo/whiz/bang', (string) Path::for('e:\\work')->path('www', 'foo')->path('whiz\\bang')); 32 | } 33 | 34 | } 35 | -------------------------------------------------------------------------------- /src/CRM/CivixBundle/Utils/PathloadPackage.php: -------------------------------------------------------------------------------- 1 | <?php 2 | 3 | namespace CRM\CivixBundle\Utils; 4 | 5 | class PathloadPackage { 6 | 7 | /** 8 | * Split a package identifier into its parts. 9 | * 10 | * @param string $package 11 | * Ex: 'foobar@1.2.3' 12 | * @return array 13 | * Tuple: [$majorName, $name, $version] 14 | * Ex: 'foobar@1', 'foobar', '1.2.3' 15 | */ 16 | public static function parseExpr(string $package): array { 17 | if (strpos($package, '@') === FALSE) { 18 | throw new \RuntimeException("Malformed package name: $package"); 19 | } 20 | [$prefix, $suffix] = explode('@', $package, 2); 21 | $prefix = str_replace('/', '~', $prefix); 22 | [$major] = explode('.', $suffix, 2); 23 | return ["$prefix@$major", $prefix, $suffix]; 24 | } 25 | 26 | public static function parseFileType(string $file): array { 27 | if (substr($file, -4) === '.php') { 28 | return ['php', substr(basename($file), 0, -4)]; 29 | } 30 | elseif (substr($file, '-5') === '.phar') { 31 | return ['phar', substr(basename($file), 0, -5)]; 32 | } 33 | elseif (is_dir($file)) { 34 | return ['dir', basename($file)]; 35 | } 36 | else { 37 | return [NULL, NULL]; 38 | } 39 | } 40 | 41 | /** 42 | * @param string $file 43 | * Ex: '/var/www/app-1/lib/foobar@.1.2.3.phar' 44 | * @return PathLoadPackage|null 45 | */ 46 | public static function create(string $file): ?PathLoadPackage { 47 | [$type, $base] = self::parseFileType($file); 48 | if ($type === NULL) { 49 | return NULL; 50 | } 51 | $self = new PathLoadPackage(); 52 | [$self->majorName, $self->name, $self->version] = static::parseExpr($base); 53 | $self->file = $file; 54 | $self->type = $type; 55 | return $self; 56 | } 57 | 58 | /** 59 | * @var string 60 | * Ex: '/var/www/app-1/lib/cloud-file-io@1.2.3.phar' 61 | */ 62 | public $file; 63 | 64 | /** 65 | * @var string 66 | * Ex: 'cloud-file-io' 67 | */ 68 | public $name; 69 | 70 | /** 71 | * @var string 72 | * Ex: 'cloud-file-io@1' 73 | */ 74 | public $majorName; 75 | 76 | /** 77 | * @var string 78 | * Ex: '1.2.3' 79 | */ 80 | public $version; 81 | 82 | /** 83 | * @var string 84 | * Ex: 'php' or 'phar' or 'dir' 85 | */ 86 | public $type; 87 | 88 | } 89 | -------------------------------------------------------------------------------- /src/CRM/CivixBundle/Utils/Versioning.php: -------------------------------------------------------------------------------- 1 | <?php 2 | 3 | namespace CRM\CivixBundle\Utils; 4 | 5 | class Versioning { 6 | 7 | /** 8 | * @param array $versions 9 | * List of versions. Ex: ['4.7', '5.40', '5.41'] 10 | * @param string $mode 11 | * Either return the lowest version ('MIN') or the highest version ('MAX'). 12 | * @return string|null 13 | */ 14 | public static function pickVer(array $versions, string $mode): ?string { 15 | usort($versions, 'version_compare'); 16 | 17 | switch ($mode) { 18 | case 'MIN': 19 | return $versions ? reset($versions) : NULL; 20 | 21 | case 'MAX': 22 | return $versions ? end($versions) : NULL; 23 | 24 | default: 25 | throw new \RuntimeException("pickVer($mode): Unrecognized mode"); 26 | } 27 | } 28 | 29 | } 30 | -------------------------------------------------------------------------------- /tests/TODO.md: -------------------------------------------------------------------------------- 1 | Civix currently does not have an automated test-suite. The aim of this file is to sketch out the logic for such a 2 | test-suite. 3 | 4 | General 5 | ======= 6 | 7 | * Civix aims to support multiple versions of CiviCRM (v4.2+). It should be possible to run the test-suite on top of 8 | any version of CiviCRM. 9 | * Main concern is end-to-end testing 10 | * For each test-run, we should start from pristine DB (a la "civibuild restore") 11 | 12 | civix generate:module 13 | ===================== 14 | 15 | ``` 16 | assert: extension is not known/active 17 | execute: civix generate:module org.example.foo 18 | execute: drush cvapi extension.install key=org.example.foo 19 | assert: extension is active 20 | ``` 21 | 22 | civix generate:upgrader 23 | ======================= 24 | 25 | ``` 26 | assert: extension is not known/active 27 | execute: civix generate:module org.example.foo 28 | execute: drush cvapi extension.install key=org.example.foo 29 | assert: db does not contain table "civicrm_example_foo" 30 | execute: civix generate:upgrader 31 | modify Upgrader.php to define table "civicrm_example_foo" 32 | execute: drush cvapi extension.upgrade 33 | assert: db does contain table "civicrm_example_foo" 34 | ``` 35 | 36 | civix generate:api 37 | ================== 38 | 39 | ``` 40 | assert: extension is not known/active 41 | execute: civix generate:module org.example.foo 42 | execute: civix generate:api Foo Bar 43 | execute: drush cvapi foo.bar 44 | assert: failure 45 | execute: drush cvapi extension.install key=org.example.foo 46 | execute: drush cvapi foo.bar 47 | assert: success 48 | ``` 49 | 50 | civix generate:custom-xml 51 | ========================= 52 | 53 | ``` 54 | assert: extension is not known/active 55 | execute: drush cvapi customgroup.create 56 | execute: druah cvapi customfield.create 57 | assert: customgroup & field exist 58 | execute: civix generate:module org.example.foo 59 | execute: civix generate:custom-xml 60 | execute: restore snapshot 61 | assert: customgroup & field do not exist 62 | execute: drush cvapi extension.install key=org.example.foo 63 | assert: customgroup & field exist 64 | ``` 65 | 66 | TODO 67 | ==== 68 | 69 | * civix generate:case-type 70 | * civix generate:entity 71 | * civix generate:form 72 | * civix generate:module 73 | * civix generate:page 74 | * civix generate:report 75 | * civix generate:report-ext 76 | * civix generate:search 77 | * civix generate:test 78 | -------------------------------------------------------------------------------- /tests/bootstrap.php: -------------------------------------------------------------------------------- 1 | <?php 2 | 3 | #### Find primary autoloader 4 | $autoloaders = array( 5 | implode(DIRECTORY_SEPARATOR, array(dirname(__DIR__), 'vendor', 'autoload.php')), 6 | implode(DIRECTORY_SEPARATOR, array(dirname(dirname(dirname(dirname(__DIR__)))), 'vendor', 'autoload.php')), 7 | ); 8 | foreach ($autoloaders as $autoloader) { 9 | if (file_exists($autoloader)) { 10 | $loader = require_once $autoloader; 11 | break; 12 | } 13 | } 14 | if (!isset($loader)) { 15 | die("Failed to find autoloader"); 16 | } 17 | 18 | #### Extra - Register classes in "tests" directory 19 | $loader->addPsr4('E2E\\', __DIR__ . '/e2e'); 20 | -------------------------------------------------------------------------------- /tests/e2e/AddManagedEntityTest.php: -------------------------------------------------------------------------------- 1 | <?php 2 | 3 | namespace E2E; 4 | 5 | use ProcessHelper\ProcessHelper; 6 | 7 | class AddManagedEntityTest extends \PHPUnit\Framework\TestCase { 8 | 9 | use CivixProjectTestTrait; 10 | 11 | public static $key = 'civix_exportmgd'; 12 | 13 | public function setUp(): void { 14 | chdir(static::getWorkspacePath()); 15 | static::cleanDir(static::getKey()); 16 | $this->civixGenerateModule(static::getKey()); 17 | chdir(static::getKey()); 18 | 19 | $this->assertFileGlobs([ 20 | 'info.xml' => 1, 21 | 'civix_exportmgd.php' => 1, 22 | 'civix_exportmgd.civix.php' => 1, 23 | ]); 24 | $this->civixMixin(['--disable-all' => TRUE]); 25 | } 26 | 27 | public function testAddMgd(): void { 28 | $this->assertMixinStatuses(['mgd-php@1' => 'off']); 29 | $this->assertFileGlobs(['managed/OptionGroup_preferred_communication_method.mgd.php' => 0]); 30 | 31 | $tester = static::civix('export'); 32 | $tester->execute(['<EntityName>' => 'OptionGroup', '<EntityId>' => 1]); 33 | if ($tester->getStatusCode() !== 0) { 34 | throw new \RuntimeException(sprintf("Failed to generate mgd (%s)", static::getKey())); 35 | } 36 | 37 | $this->assertMixinStatuses(['mgd-php@1' => 'on']); 38 | $this->assertFileGlobs(['managed/OptionGroup_preferred_communication_method.mgd.php' => 1]); 39 | 40 | ProcessHelper::runOk('php -l managed/OptionGroup_preferred_communication_method.mgd.php'); 41 | $expectPhrases = [ 42 | "use CRM_CivixExportmgd_ExtensionUtil as E;", 43 | "'title' => E::ts('Preferred Communication Method')", 44 | "'option_group_id.name' => 'preferred_communication_method'", 45 | "'label' => E::ts('Phone')", 46 | "'value' => '1'", 47 | "'label' => E::ts('Email')", 48 | "'value' => '2'", 49 | ]; 50 | $this->assertStringSequence($expectPhrases, file_get_contents('managed/OptionGroup_preferred_communication_method.mgd.php')); 51 | } 52 | 53 | } 54 | -------------------------------------------------------------------------------- /tests/e2e/AddPageTest.php: -------------------------------------------------------------------------------- 1 | <?php 2 | 3 | namespace E2E; 4 | 5 | class AddPageTest extends \PHPUnit\Framework\TestCase { 6 | 7 | use CivixProjectTestTrait; 8 | 9 | public static $key = 'civix_addpage'; 10 | 11 | public function setUp(): void { 12 | chdir(static::getWorkspacePath()); 13 | static::cleanDir(static::getKey()); 14 | $this->civixGenerateModule(static::getKey()); 15 | chdir(static::getKey()); 16 | 17 | $this->assertFileGlobs([ 18 | 'info.xml' => 1, 19 | 'civix_addpage.php' => 1, 20 | 'civix_addpage.civix.php' => 1, 21 | ]); 22 | } 23 | 24 | public function testAddPage(): void { 25 | $this->assertFileGlobs([ 26 | 'CRM/CivixAddpage/Page/MyPage.php' => 0, 27 | 'templates/CRM/CivixAddpage/Page/MyPage.tpl' => 0, 28 | 'xml/Menu/civix_addpage.xml' => 0, 29 | ]); 30 | 31 | $this->civixGeneratePage('MyPage', 'civicrm/thirty'); 32 | 33 | $this->assertFileGlobs([ 34 | 'CRM/CivixAddpage/Page/MyPage.php' => 1, 35 | 'templates/CRM/CivixAddpage/Page/MyPage.tpl' => 1, 36 | 'xml/Menu/civix_addpage.xml' => 1, 37 | ]); 38 | } 39 | 40 | } 41 | -------------------------------------------------------------------------------- /tests/e2e/AddServiceTest.php: -------------------------------------------------------------------------------- 1 | <?php 2 | 3 | namespace E2E; 4 | 5 | class AddServiceTest extends \PHPUnit\Framework\TestCase { 6 | 7 | use CivixProjectTestTrait; 8 | 9 | public static $key = 'civix_addsvc'; 10 | 11 | public function setUp(): void { 12 | chdir(static::getWorkspacePath()); 13 | static::cleanDir(static::getKey()); 14 | $this->civixGenerateModule(static::getKey()); 15 | chdir(static::getKey()); 16 | 17 | $this->assertFileGlobs([ 18 | 'info.xml' => 1, 19 | 'civix_addsvc.php' => 1, 20 | 'civix_addsvc.civix.php' => 1, 21 | ]); 22 | } 23 | 24 | public function testAddService(): void { 25 | $this->assertFileGlobs([ 26 | 'CRM/CivixAddsvc/Whiz/Bang.php' => 0, 27 | ]); 28 | 29 | $this->civixGenerateService('civix_addsvc.whiz.bang'); 30 | 31 | $this->assertFileGlobs([ 32 | 'CRM/CivixAddsvc/Whiz/Bang.php' => 1, 33 | ]); 34 | 35 | $code = file_get_contents('CRM/CivixAddsvc/Whiz/Bang.php'); 36 | $expect = [ 37 | 'use CRM_CivixAddsvc_ExtensionUtil as E', 38 | 'use Civi\Core\Service\AutoService', 39 | 'use Symfony\Component\EventDispatcher\EventSubscriberInterface', 40 | '@service civix_addsvc.whiz.bang', 41 | 'class CRM_CivixAddsvc_Whiz_Bang extends AutoService implements EventSubscriberInterface', 42 | 'function getSubscribedEvents', 43 | ]; 44 | $this->assertStringSequence($expect, $code); 45 | } 46 | 47 | } 48 | -------------------------------------------------------------------------------- /tests/e2e/CRMNamingTest.php: -------------------------------------------------------------------------------- 1 | <?php 2 | 3 | namespace E2E; 4 | 5 | use CRM\CivixBundle\Builder\Info; 6 | 7 | class CRMNamingTest extends \PHPUnit\Framework\TestCase { 8 | 9 | use CivixProjectTestTrait; 10 | 11 | public static $key = 'civix_crmnaming'; 12 | 13 | /** 14 | * @var \CRM\CivixBundle\Generator 15 | */ 16 | protected $upgrader; 17 | 18 | public function setUp(): void { 19 | chdir(static::getWorkspacePath()); 20 | static::cleanDir(static::getKey()); 21 | $this->civixGenerateModule(static::getKey()); 22 | chdir(static::getKey()); 23 | 24 | $this->assertFileGlobs([ 25 | 'info.xml' => 1, 26 | 'civix_crmnaming.php' => 1, 27 | 'civix_crmnaming.civix.php' => 1, 28 | ]); 29 | 30 | \Civix::ioStack()->push(...$this->createInputOutput()); 31 | $this->upgrader = \Civix::generator(static::getExtPath()); 32 | $this->upgrader->updateInfo(function(Info $info) { 33 | // FIXME: Allow "_" instead of "/" 34 | $info->get()->civix->namespace = 'CRM/NamingTest'; 35 | }); 36 | } 37 | 38 | protected function tearDown(): void { 39 | parent::tearDown(); 40 | \Civix::ioStack()->reset(); 41 | } 42 | 43 | public function testNaming_OnePart(): void { 44 | $vars = $this->upgrader->createClassVars('CRM_NamingTest_Widget'); 45 | $this->assertTrue(is_string($vars['extBaseDir']) && is_dir($vars['extBaseDir'])); 46 | $this->assertEquals('civix_crmnaming', $vars['extMainFile']); 47 | $this->assertEquals('civix_crmnaming', $vars['extKey']); 48 | $this->assertEquals('CRM/NamingTest/Widget.php', $vars['classFile']); 49 | $this->assertEquals('CRM_NamingTest_Widget', $vars['className']); 50 | $this->assertEquals('CRM_NamingTest_Widget', $vars['classNameFull']); 51 | $this->assertEquals('', $vars['classNamespace']); 52 | $this->assertEquals('', $vars['classNamespaceDecl']); 53 | $this->assertEquals('use CRM_NamingTest_ExtensionUtil as E;', $vars['useE']); 54 | } 55 | 56 | public function testNaming_TwoParts(): void { 57 | $vars = $this->upgrader->createClassVars('CRM_NamingTest_Widget_Gizmo'); 58 | $this->assertTrue(is_string($vars['extBaseDir']) && is_dir($vars['extBaseDir'])); 59 | $this->assertEquals('civix_crmnaming', $vars['extMainFile']); 60 | $this->assertEquals('civix_crmnaming', $vars['extKey']); 61 | $this->assertEquals('CRM/NamingTest/Widget/Gizmo.php', $vars['classFile']); 62 | $this->assertEquals('CRM_NamingTest_Widget_Gizmo', $vars['className']); 63 | $this->assertEquals('CRM_NamingTest_Widget_Gizmo', $vars['classNameFull']); 64 | $this->assertEquals('', $vars['classNamespace']); 65 | $this->assertEquals('', $vars['classNamespaceDecl']); 66 | $this->assertEquals('use CRM_NamingTest_ExtensionUtil as E;', $vars['useE']); 67 | } 68 | 69 | } 70 | -------------------------------------------------------------------------------- /tests/e2e/CiviNamingTest.php: -------------------------------------------------------------------------------- 1 | <?php 2 | 3 | namespace E2E; 4 | 5 | use CRM\CivixBundle\Builder\Info; 6 | 7 | class CiviNamingTest extends \PHPUnit\Framework\TestCase { 8 | 9 | use CivixProjectTestTrait; 10 | 11 | public static $key = 'civix_civinaming'; 12 | 13 | /** 14 | * @var \CRM\CivixBundle\Generator 15 | */ 16 | protected $upgrader; 17 | 18 | public function setUp(): void { 19 | chdir(static::getWorkspacePath()); 20 | static::cleanDir(static::getKey()); 21 | $this->civixGenerateModule(static::getKey()); 22 | chdir(static::getKey()); 23 | 24 | $this->assertFileGlobs([ 25 | 'info.xml' => 1, 26 | 'civix_civinaming.php' => 1, 27 | 'civix_civinaming.civix.php' => 1, 28 | ]); 29 | 30 | \Civix::ioStack()->push(...$this->createInputOutput()); 31 | $this->upgrader = \Civix::generator(static::getExtPath()); 32 | $this->upgrader->updateInfo(function(Info $info) { 33 | // FIXME: Allow "\" instead of "/" 34 | $info->get()->civix->namespace = 'Civi/NamingTest'; 35 | }); 36 | } 37 | 38 | protected function tearDown(): void { 39 | parent::tearDown(); 40 | \Civix::ioStack()->reset(); 41 | } 42 | 43 | public function testNaming_OnePart(): void { 44 | $vars = $this->upgrader->createClassVars('Civi\\NamingTest\\Widget'); 45 | $this->assertTrue(is_string($vars['extBaseDir']) && is_dir($vars['extBaseDir'])); 46 | $this->assertEquals('civix_civinaming', $vars['extMainFile']); 47 | $this->assertEquals('civix_civinaming', $vars['extKey']); 48 | $this->assertEquals('Civi/NamingTest/Widget.php', $vars['classFile']); 49 | $this->assertEquals('Widget', $vars['className']); 50 | $this->assertEquals('Civi\\NamingTest\\Widget', $vars['classNameFull']); 51 | $this->assertEquals('Civi\\NamingTest', $vars['classNamespace']); 52 | $this->assertEquals('namespace Civi\\NamingTest;', $vars['classNamespaceDecl']); 53 | $this->assertEquals('use CRM_NamingTest_ExtensionUtil as E;', $vars['useE']); 54 | } 55 | 56 | public function testNaming_TwoParts(): void { 57 | $vars = $this->upgrader->createClassVars('Civi\\NamingTest\\Widget\\Gizmo'); 58 | $this->assertTrue(is_string($vars['extBaseDir']) && is_dir($vars['extBaseDir'])); 59 | $this->assertEquals('civix_civinaming', $vars['extMainFile']); 60 | $this->assertEquals('civix_civinaming', $vars['extKey']); 61 | $this->assertEquals('Civi/NamingTest/Widget/Gizmo.php', $vars['classFile']); 62 | $this->assertEquals('Gizmo', $vars['className']); 63 | $this->assertEquals('Civi\\NamingTest\\Widget\\Gizmo', $vars['classNameFull']); 64 | $this->assertEquals('Civi\\NamingTest\\Widget', $vars['classNamespace']); 65 | $this->assertEquals('namespace Civi\\NamingTest\\Widget;', $vars['classNamespaceDecl']); 66 | $this->assertEquals('use CRM_NamingTest_ExtensionUtil as E;', $vars['useE']); 67 | } 68 | 69 | } 70 | -------------------------------------------------------------------------------- /tests/e2e/CleanEmptyTest.in.txt: -------------------------------------------------------------------------------- 1 | <?php 2 | // [[ EX - Keep pure comment ]] 3 | 4 | // function civixcleanempty_civicrm_keepPureComment() { 5 | // foo 6 | // } 7 | 8 | // [[ EX - Removable 1 ]] 9 | 10 | function civixcleanempty_civicrm_removeOne() { 11 | } 12 | 13 | // [[ EX - Keepable 1 ]] 14 | 15 | function civixcleanempty_civicrm_keepOne() { 16 | stuff(); 17 | } 18 | 19 | // [[ EX - Removable 2 ]] 20 | 21 | /** 22 | * 23 | */ 24 | function civixcleanempty_civicrm_removeTwo() { 25 | 26 | } 27 | 28 | // [[ EX - Keepable 2 ]] 29 | 30 | /** 31 | * @param $x 32 | */ 33 | function civixcleanempty_civicrm_keepTwo($x) { 34 | stuff($x); 35 | } 36 | 37 | // [[ EX - Removable 3 ]] 38 | 39 | /** 40 | * 41 | */ 42 | function civixcleanempty_civicrm_remove_three($x) { 43 | // Do nothing 44 | } 45 | 46 | // [[ EX - Keepable 3 ]] 47 | 48 | function civixcleanempty_civicrm_keepThree() { 49 | stuff(); // Comment 50 | } 51 | 52 | // [[ EX - Removable 4 ]] 53 | 54 | function civixcleanempty_civicrm_removeFour() {} 55 | 56 | // [[ EX - Keepable 4 ]] 57 | 58 | function civixcleanempty_civicrm_keepFour() { 59 | foreach ($a as $b) {} x(); 60 | } 61 | 62 | // [[ EX - Removable 5 ]] 63 | 64 | function civixcleanempty_civicrm_removeFive() { } 65 | 66 | // [[ EX - Keepable 5 ]] 67 | 68 | function civixcleanempty_civicrm_keepFive() { 69 | if (FOO) { 70 | x(); 71 | } 72 | y(); 73 | } 74 | 75 | // [[ EX - Removable 6 ]] 76 | 77 | function civixcleanempty_civicrm_removeSix() { 78 | return; 79 | } 80 | 81 | // [[ EX - Keepable 6 ]] 82 | 83 | function civixcleanempty_civicrm_keepSix() { 84 | return; 85 | $x++; 86 | } 87 | 88 | // [[ EX - Keepable 6a ]] 89 | 90 | function civixcleanempty_civicrm_keepSixA() { 91 | return 1; 92 | } 93 | -------------------------------------------------------------------------------- /tests/e2e/CleanEmptyTest.out.txt: -------------------------------------------------------------------------------- 1 | <?php 2 | // [[ EX - Keep pure comment ]] 3 | 4 | // function civixcleanempty_civicrm_keepPureComment() { 5 | // foo 6 | // } 7 | 8 | // [[ EX - Removable 1 ]] 9 | 10 | // [[ EX - Keepable 1 ]] 11 | 12 | function civixcleanempty_civicrm_keepOne() { 13 | stuff(); 14 | } 15 | 16 | // [[ EX - Removable 2 ]] 17 | 18 | 19 | 20 | // [[ EX - Keepable 2 ]] 21 | 22 | /** 23 | * @param $x 24 | */ 25 | function civixcleanempty_civicrm_keepTwo($x) { 26 | stuff($x); 27 | } 28 | 29 | // [[ EX - Removable 3 ]] 30 | 31 | 32 | 33 | // [[ EX - Keepable 3 ]] 34 | 35 | function civixcleanempty_civicrm_keepThree() { 36 | stuff(); // Comment 37 | } 38 | 39 | // [[ EX - Removable 4 ]] 40 | 41 | // [[ EX - Keepable 4 ]] 42 | 43 | function civixcleanempty_civicrm_keepFour() { 44 | foreach ($a as $b) {} x(); 45 | } 46 | 47 | // [[ EX - Removable 5 ]] 48 | 49 | // [[ EX - Keepable 5 ]] 50 | 51 | function civixcleanempty_civicrm_keepFive() { 52 | if (FOO) { 53 | x(); 54 | } 55 | y(); 56 | } 57 | 58 | // [[ EX - Removable 6 ]] 59 | 60 | // [[ EX - Keepable 6 ]] 61 | 62 | function civixcleanempty_civicrm_keepSix() { 63 | return; 64 | $x++; 65 | } 66 | 67 | // [[ EX - Keepable 6a ]] 68 | 69 | function civixcleanempty_civicrm_keepSixA() { 70 | return 1; 71 | } 72 | -------------------------------------------------------------------------------- /tests/e2e/CleanEmptyTest.php: -------------------------------------------------------------------------------- 1 | <?php 2 | 3 | namespace E2E; 4 | 5 | class CleanEmptyTest extends \PHPUnit\Framework\TestCase { 6 | 7 | use CivixProjectTestTrait; 8 | 9 | public static $key = 'civixcleanempty'; 10 | 11 | public function setUp(): void { 12 | chdir(static::getWorkspacePath()); 13 | static::cleanDir(static::getKey()); 14 | $this->civixGenerateModule(static::getKey()); 15 | chdir(static::getKey()); 16 | 17 | $this->assertFileExists('info.xml'); 18 | } 19 | 20 | public function testCleanup(): void { 21 | $mainPhp = static::getKey() . '.php'; 22 | copy(__DIR__ . '/CleanEmptyTest.in.txt', $mainPhp); 23 | $this->assertEquals(0, $this->civix('upgrade')->execute([])); 24 | $this->assertFileEquals(__DIR__ . '/CleanEmptyTest.out.txt', $mainPhp); 25 | } 26 | 27 | } 28 | -------------------------------------------------------------------------------- /tests/e2e/IdempotentUpgradeTest.php: -------------------------------------------------------------------------------- 1 | <?php 2 | 3 | namespace E2E; 4 | 5 | /** 6 | * What happens if you take a new extension and run an upgrade on it? 7 | * The result should match. 8 | */ 9 | class IdempotentUpgradeTest extends \PHPUnit\Framework\TestCase { 10 | 11 | use CivixProjectTestTrait; 12 | 13 | public static $key = 'civix_upgradereset'; 14 | 15 | public function setUp(): void { 16 | chdir(static::getWorkspacePath()); 17 | static::cleanDir(static::getKey()); 18 | $this->civixGenerateModule(static::getKey(), ['--compatibility' => '5.0']); 19 | chdir(static::getKey()); 20 | } 21 | 22 | /** 23 | * Do the upgrade a full replay (`civix upgrade --start=0`). 24 | */ 25 | public function testBasicUpgrade(): void { 26 | // Make an example 27 | $this->civixGeneratePage('MyPage', 'civicrm/thirty'); 28 | $this->civixGenerateUpgrader(); /* TODO: Make this implicit with generate:entity */ 29 | $this->civixGenerateEntity('MyEntity'); 30 | $start = $this->getExtSnapshot(); 31 | 32 | // Do the upgrade 33 | $result = $this->civixUpgrade()->getDisplay(TRUE); 34 | $expectLines = [ 35 | 'Incremental upgrades', 36 | 'General upgrade', 37 | ]; 38 | $this->assertStringSequence($expectLines, $result); 39 | $this->assertDoesNotMatchRegularExpression(';Upgrade v([\d\.]+) => v([\d\.]+);', $result); 40 | 41 | // Compare before+after 42 | $end = $this->getExtSnapshot(); 43 | $this->assertEquals($start, $end); 44 | } 45 | 46 | /** 47 | * Do the upgrade a full replay (`civix upgrade --start=0`). 48 | */ 49 | public function testResetVersion0(): void { 50 | // Make an example 51 | $this->civixGeneratePage('MyPage', 'civicrm/thirty'); 52 | $this->civixGenerateUpgrader(); /* TODO: Make this implicit with generate:entity */ 53 | $this->civixGenerateEntity('MyEntity'); 54 | $start = $this->getExtSnapshot(); 55 | 56 | // Do the upgrade 57 | $result = $this->civixUpgrade(['--start' => '0'])->getDisplay(TRUE); 58 | $expectLines = [ 59 | 'Incremental upgrades', 60 | 'Upgrade v13.10.0 => v16.10.0', 61 | 'Upgrade v22.05.0 => v22.05.2', 62 | 'General upgrade', 63 | ]; 64 | $this->assertStringSequence($expectLines, $result); 65 | 66 | // Compare before+after 67 | $end = $this->getExtSnapshot(); 68 | $this->assertEquals($start, $end); 69 | } 70 | 71 | /** 72 | * Do the upgrade a full replay (`civix upgrade --start=22.01.0`). 73 | */ 74 | public function testResetVersion2201(): void { 75 | // Make an example 76 | $this->civixGeneratePage('MyPage', 'civicrm/thirty'); 77 | $this->civixGenerateUpgrader(); /* TODO: Make this implicit with generate:entity */ 78 | $this->civixGenerateEntity('MyEntity'); 79 | $start = $this->getExtSnapshot(); 80 | 81 | // Do the upgrade 82 | $result = $this->civixUpgrade(['--start' => '22.01.0'])->getDisplay(TRUE); 83 | $expectLines = [ 84 | 'Incremental upgrades', 85 | 'Upgrade v22.05.0 => v22.05.2', 86 | 'General upgrade', 87 | ]; 88 | $this->assertStringSequence($expectLines, $result); 89 | $this->assertStringNotContainsString('Upgrade v13.10.0 => v16.10.0', $result); 90 | 91 | // Compare before+after 92 | $end = $this->getExtSnapshot(); 93 | $this->assertEquals($start, $end); 94 | } 95 | 96 | } 97 | -------------------------------------------------------------------------------- /tests/e2e/MultiExtensionTest.php: -------------------------------------------------------------------------------- 1 | <?php 2 | 3 | namespace E2E; 4 | 5 | use ProcessHelper\ProcessHelper as PH; 6 | 7 | /** 8 | * For this test, we create and enable two extensions. 9 | * They should coexist. 10 | */ 11 | class MultiExtensionTest extends \PHPUnit\Framework\TestCase { 12 | 13 | use CivixProjectTestTrait; 14 | 15 | public static $keys = ['apple', 'banana']; 16 | 17 | public static $key = '**INVALID**'; 18 | 19 | public function setUp(): void { 20 | chdir(static::getWorkspacePath()); 21 | PH::runOk('civibuild restore'); 22 | 23 | foreach (static::$keys as $key) { 24 | static::cleanDir($key); 25 | $this->civixGenerateModule($key); 26 | } 27 | 28 | foreach ($this->visitExts() as $name) { 29 | $this->assertFileGlobs([ 30 | 'info.xml' => 1, 31 | "$name.php" => 1, 32 | "$name.civix.php" => 1, 33 | ]); 34 | } 35 | } 36 | 37 | protected function tearDown(): void { 38 | chdir(static::getWorkspacePath()); 39 | PH::runOk('civibuild restore'); 40 | \Civix::ioStack()->reset(); 41 | } 42 | 43 | public function testAddPage(): void { 44 | foreach ($this->visitExts() as $name) { 45 | $camel = ucfirst($name); 46 | $this->assertFileGlobs(["CRM/$camel/Page/My$camel.php" => 0]); 47 | $this->civixGeneratePage("My$camel", "civicrm/example/$name"); 48 | $this->assertFileGlobs(["CRM/$camel/Page/My$camel.php" => 1]); 49 | 50 | PH::runOK('cv en ' . escapeshellarg($name)); 51 | } 52 | 53 | foreach ($this->visitExts() as $name) { 54 | $camel = ucfirst($name); 55 | $getPage = PH::runOK("cv api4 Route.get +w path=civicrm/example/$name +s page_callback"); 56 | $this->assertTrue((bool) preg_match("/CRM_{$camel}_Page_My{$camel}/", $getPage->getOutput()), "Route 'civicrm/example/$name' should be registered"); 57 | } 58 | } 59 | 60 | protected function visitExts(): \Generator { 61 | foreach (static::$keys as $key) { 62 | static::$key = $key; 63 | chdir(static::getWorkspacePath()); 64 | chdir($key); 65 | yield $key; 66 | } 67 | chdir(static::getWorkspacePath()); 68 | } 69 | 70 | } 71 | -------------------------------------------------------------------------------- /tests/scenarios/README.md: -------------------------------------------------------------------------------- 1 | Define extra tests to supplement the `make-snapshots.sh` process. 2 | 3 | When running `make-snapshots.sh`, you can give a list of scenarios like `qf` 4 | and `entity4`. Each corresponds to a subfolder. For example, the `entity4` 5 | scenario would have a structure like this 6 | 7 | * `./tests/scenarios/entity4/` 8 | * `make.sh` (generate a new extension; mandatory) 9 | * `phpunit.xml.dist` (phpunit config; recommended) 10 | * `bootstrap.php` (phpunit bootstrap; recommended) 11 | * `MyTest.php` (phpunit test-case; recommended) 12 | -------------------------------------------------------------------------------- /tests/scenarios/empty/make.sh: -------------------------------------------------------------------------------- 1 | echo "Nothing to add" -------------------------------------------------------------------------------- /tests/scenarios/entity4/Entity4Test.php: -------------------------------------------------------------------------------- 1 | <?php 2 | 3 | use CRM_Civixsnapshot_ExtensionUtil as E; 4 | 5 | /** 6 | * @group e2e 7 | */ 8 | class Entity4Test extends \PHPUnit\Framework\TestCase { 9 | 10 | public function setUp(): void { 11 | parent::setUp(); 12 | $mapper = \CRM_Extension_System::singleton()->getMapper(); 13 | $this->assertTrue($mapper->isActiveModule('civixsnapshot'), 'Extension civixsnapshot should already be active.'); 14 | } 15 | 16 | public function testSchemaExists() { 17 | $table = 'civicrm_my_entity_four'; 18 | $exists = \CRM_Core_DAO::checkTableExists($table); 19 | $this->assertTrue($exists, "Table $table should exist"); 20 | } 21 | 22 | public function testAlterSchemaField() { 23 | $this->assertEquals(FALSE, CRM_Core_BAO_SchemaHandler::checkIfFieldExists('civicrm_my_entity_four', 'extra_string'), 'extra_string should not yet exist'); 24 | E::schema()->alterSchemaField('MyEntityFour', 'extra_string', [ 25 | 'title' => ts('Example'), 26 | 'sql_type' => 'varchar(64)', 27 | 'input_type' => 'Text', 28 | 'required' => FALSE, 29 | ]); 30 | $this->assertEquals(TRUE, CRM_Core_BAO_SchemaHandler::checkIfFieldExists('civicrm_my_entity_four', 'extra_string'), 'extra_string should exist'); 31 | } 32 | 33 | } 34 | -------------------------------------------------------------------------------- /tests/scenarios/entity4/bootstrap.php: -------------------------------------------------------------------------------- 1 | <?php 2 | 3 | ini_set('memory_limit', '2G'); 4 | ini_set('safe_mode', 0); 5 | define('CIVICRM_CONTAINER_CACHE', 'never'); 6 | // eval(cv('php:boot --level=classloader', 'phpcode')); 7 | eval(cv('php:boot', 'phpcode')); 8 | 9 | /** 10 | * Call the "cv" command. 11 | * 12 | * @param string $cmd 13 | * The rest of the command to send. 14 | * @param string $decode 15 | * Ex: 'json' or 'phpcode'. 16 | * @return string 17 | * Response output (if the command executed normally). 18 | * @throws \RuntimeException 19 | * If the command terminates abnormally. 20 | */ 21 | function cv($cmd, $decode = 'json') { 22 | $cmd = 'cv ' . $cmd; 23 | $descriptorSpec = [0 => ["pipe", "r"], 1 => ["pipe", "w"], 2 => STDERR]; 24 | $oldOutput = getenv('CV_OUTPUT'); 25 | putenv("CV_OUTPUT=json"); 26 | printf("pwd=%s\n", getcwd()); 27 | $process = proc_open($cmd, $descriptorSpec, $pipes, __DIR__); 28 | putenv("CV_OUTPUT=$oldOutput"); 29 | fclose($pipes[0]); 30 | $result = stream_get_contents($pipes[1]); 31 | fclose($pipes[1]); 32 | if (proc_close($process) !== 0) { 33 | throw new RuntimeException("Command failed ($cmd):\n$result"); 34 | } 35 | switch ($decode) { 36 | case 'raw': 37 | return $result; 38 | 39 | case 'phpcode': 40 | // If the last output is /*PHPCODE*/, then we managed to complete execution. 41 | if (substr(trim($result), 0, 12) !== "/*BEGINPHP*/" || substr(trim($result), -10) !== "/*ENDPHP*/") { 42 | throw new \RuntimeException("Command failed ($cmd):\n$result"); 43 | } 44 | return $result; 45 | 46 | case 'json': 47 | return json_decode($result, 1); 48 | 49 | default: 50 | throw new RuntimeException("Bad decoder format ($decode)"); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /tests/scenarios/entity4/make.sh: -------------------------------------------------------------------------------- 1 | $CIVIX $VERBOSITY generate:upgrader 2 | $CIVIX $VERBOSITY generate:entity MyEntityFour -------------------------------------------------------------------------------- /tests/scenarios/entity4/phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | <?xml version="1.0"?> 2 | <phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 3 | backupGlobals="false" 4 | backupStaticAttributes="false" 5 | colors="true" 6 | convertErrorsToExceptions="true" 7 | convertNoticesToExceptions="true" 8 | convertWarningsToExceptions="true" 9 | processIsolation="false" 10 | stopOnFailure="false" 11 | bootstrap="bootstrap.php" 12 | cacheResult="false" 13 | xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/9.3/phpunit.xsd"> 14 | <coverage> 15 | <include> 16 | <directory suffix=".php">./src</directory> 17 | </include> 18 | </coverage> 19 | <testsuites> 20 | <testsuite name="Entity4 Test Suite"> 21 | <directory>./</directory> 22 | </testsuite> 23 | </testsuites> 24 | <listeners> 25 | <listener class="Civi\Test\CiviTestListener"> 26 | <arguments/> 27 | </listener> 28 | </listeners> 29 | </phpunit> 30 | -------------------------------------------------------------------------------- /tests/scenarios/kitchensink/make.sh: -------------------------------------------------------------------------------- 1 | $CIVIX $VERBOSITY generate:api MyEntity Myaction 2 | $CIVIX $VERBOSITY generate:api MyEntity myaction2 3 | $CIVIX $VERBOSITY generate:case-type MyLabel MyName 4 | # $CIVIX $VERBOSITY generate:custom-xml -f --data="FIXME" --uf="FIXME" 5 | $CIVIX $VERBOSITY generate:entity MyEntityFour 6 | $CIVIX $VERBOSITY generate:form MyForm civicrm/my-form 7 | $CIVIX $VERBOSITY generate:form My_StuffyForm civicrm/my-stuffy-form 8 | $CIVIX $VERBOSITY generate:page MyPage civicrm/my-page 9 | $CIVIX $VERBOSITY generate:report MyReport CiviContribute 10 | $CIVIX $VERBOSITY generate:search MySearch 11 | $CIVIX $VERBOSITY generate:test --template=headless 'Civi\Civiexample\BarTest' 12 | $CIVIX $VERBOSITY generate:test --template=e2e 'Civi\Civiexample\EndTest' 13 | $CIVIX $VERBOSITY generate:test --template=phpunit 'Civi\CiviExample\PHPUnitTest' 14 | $CIVIX $VERBOSITY generate:upgrader 15 | $CIVIX $VERBOSITY generate:angular-module 16 | $CIVIX $VERBOSITY generate:angular-page FooCtrl foo 17 | $CIVIX $VERBOSITY generate:angular-directive foo-bar 18 | $CIVIX $VERBOSITY generate:theme 19 | $CIVIX $VERBOSITY generate:theme extratheme -------------------------------------------------------------------------------- /tests/scenarios/qf/make.sh: -------------------------------------------------------------------------------- 1 | $CIVIX $VERBOSITY generate:page MyPage civicrm/my-page 2 | $CIVIX $VERBOSITY generate:form MyForm civicrm/my-form -------------------------------------------------------------------------------- /tests/scenarios/svc/make.sh: -------------------------------------------------------------------------------- 1 | $CIVIX $VERBOSITY generate:service some.thing --naming=Civi --no-interaction -------------------------------------------------------------------------------- /tests/snapshots/org.example.civixsnapshot-v16.02.0-empty/original.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/totten/civix/b4c7e5fe832cb88a91b503a6f7ea436e7637f749/tests/snapshots/org.example.civixsnapshot-v16.02.0-empty/original.zip -------------------------------------------------------------------------------- /tests/snapshots/org.example.civixsnapshot-v16.02.0-kitchensink/original.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/totten/civix/b4c7e5fe832cb88a91b503a6f7ea436e7637f749/tests/snapshots/org.example.civixsnapshot-v16.02.0-kitchensink/original.zip -------------------------------------------------------------------------------- /tests/snapshots/org.example.civixsnapshot-v16.02.0-qf/original.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/totten/civix/b4c7e5fe832cb88a91b503a6f7ea436e7637f749/tests/snapshots/org.example.civixsnapshot-v16.02.0-qf/original.zip -------------------------------------------------------------------------------- /tests/snapshots/org.example.civixsnapshot-v17.10.5-empty/original.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/totten/civix/b4c7e5fe832cb88a91b503a6f7ea436e7637f749/tests/snapshots/org.example.civixsnapshot-v17.10.5-empty/original.zip -------------------------------------------------------------------------------- /tests/snapshots/org.example.civixsnapshot-v17.10.5-kitchensink/original.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/totten/civix/b4c7e5fe832cb88a91b503a6f7ea436e7637f749/tests/snapshots/org.example.civixsnapshot-v17.10.5-kitchensink/original.zip -------------------------------------------------------------------------------- /tests/snapshots/org.example.civixsnapshot-v17.10.5-qf/original.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/totten/civix/b4c7e5fe832cb88a91b503a6f7ea436e7637f749/tests/snapshots/org.example.civixsnapshot-v17.10.5-qf/original.zip -------------------------------------------------------------------------------- /tests/snapshots/org.example.civixsnapshot-v18.12.0-empty/original.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/totten/civix/b4c7e5fe832cb88a91b503a6f7ea436e7637f749/tests/snapshots/org.example.civixsnapshot-v18.12.0-empty/original.zip -------------------------------------------------------------------------------- /tests/snapshots/org.example.civixsnapshot-v18.12.0-entity3/original.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/totten/civix/b4c7e5fe832cb88a91b503a6f7ea436e7637f749/tests/snapshots/org.example.civixsnapshot-v18.12.0-entity3/original.zip -------------------------------------------------------------------------------- /tests/snapshots/org.example.civixsnapshot-v18.12.0-kitchensink/original.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/totten/civix/b4c7e5fe832cb88a91b503a6f7ea436e7637f749/tests/snapshots/org.example.civixsnapshot-v18.12.0-kitchensink/original.zip -------------------------------------------------------------------------------- /tests/snapshots/org.example.civixsnapshot-v18.12.0-qf/original.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/totten/civix/b4c7e5fe832cb88a91b503a6f7ea436e7637f749/tests/snapshots/org.example.civixsnapshot-v18.12.0-qf/original.zip -------------------------------------------------------------------------------- /tests/snapshots/org.example.civixsnapshot-v19.11.0-empty/original.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/totten/civix/b4c7e5fe832cb88a91b503a6f7ea436e7637f749/tests/snapshots/org.example.civixsnapshot-v19.11.0-empty/original.zip -------------------------------------------------------------------------------- /tests/snapshots/org.example.civixsnapshot-v19.11.0-entity3/original.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/totten/civix/b4c7e5fe832cb88a91b503a6f7ea436e7637f749/tests/snapshots/org.example.civixsnapshot-v19.11.0-entity3/original.zip -------------------------------------------------------------------------------- /tests/snapshots/org.example.civixsnapshot-v19.11.0-kitchensink/original.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/totten/civix/b4c7e5fe832cb88a91b503a6f7ea436e7637f749/tests/snapshots/org.example.civixsnapshot-v19.11.0-kitchensink/original.zip -------------------------------------------------------------------------------- /tests/snapshots/org.example.civixsnapshot-v19.11.0-qf/original.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/totten/civix/b4c7e5fe832cb88a91b503a6f7ea436e7637f749/tests/snapshots/org.example.civixsnapshot-v19.11.0-qf/original.zip -------------------------------------------------------------------------------- /tests/snapshots/org.example.civixsnapshot-v20.09.0-empty/original.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/totten/civix/b4c7e5fe832cb88a91b503a6f7ea436e7637f749/tests/snapshots/org.example.civixsnapshot-v20.09.0-empty/original.zip -------------------------------------------------------------------------------- /tests/snapshots/org.example.civixsnapshot-v20.09.0-entity3/original.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/totten/civix/b4c7e5fe832cb88a91b503a6f7ea436e7637f749/tests/snapshots/org.example.civixsnapshot-v20.09.0-entity3/original.zip -------------------------------------------------------------------------------- /tests/snapshots/org.example.civixsnapshot-v20.09.0-kitchensink/original.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/totten/civix/b4c7e5fe832cb88a91b503a6f7ea436e7637f749/tests/snapshots/org.example.civixsnapshot-v20.09.0-kitchensink/original.zip -------------------------------------------------------------------------------- /tests/snapshots/org.example.civixsnapshot-v20.09.0-qf/original.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/totten/civix/b4c7e5fe832cb88a91b503a6f7ea436e7637f749/tests/snapshots/org.example.civixsnapshot-v20.09.0-qf/original.zip -------------------------------------------------------------------------------- /tests/snapshots/org.example.civixsnapshot-v22.02.0-empty/original.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/totten/civix/b4c7e5fe832cb88a91b503a6f7ea436e7637f749/tests/snapshots/org.example.civixsnapshot-v22.02.0-empty/original.zip -------------------------------------------------------------------------------- /tests/snapshots/org.example.civixsnapshot-v22.02.0-entity3/original.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/totten/civix/b4c7e5fe832cb88a91b503a6f7ea436e7637f749/tests/snapshots/org.example.civixsnapshot-v22.02.0-entity3/original.zip -------------------------------------------------------------------------------- /tests/snapshots/org.example.civixsnapshot-v22.02.0-entity34/original.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/totten/civix/b4c7e5fe832cb88a91b503a6f7ea436e7637f749/tests/snapshots/org.example.civixsnapshot-v22.02.0-entity34/original.zip -------------------------------------------------------------------------------- /tests/snapshots/org.example.civixsnapshot-v22.02.0-kitchensink/original.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/totten/civix/b4c7e5fe832cb88a91b503a6f7ea436e7637f749/tests/snapshots/org.example.civixsnapshot-v22.02.0-kitchensink/original.zip -------------------------------------------------------------------------------- /tests/snapshots/org.example.civixsnapshot-v22.02.0-qf/original.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/totten/civix/b4c7e5fe832cb88a91b503a6f7ea436e7637f749/tests/snapshots/org.example.civixsnapshot-v22.02.0-qf/original.zip -------------------------------------------------------------------------------- /tests/snapshots/org.example.civixsnapshot-v22.06.0-empty/original.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/totten/civix/b4c7e5fe832cb88a91b503a6f7ea436e7637f749/tests/snapshots/org.example.civixsnapshot-v22.06.0-empty/original.zip -------------------------------------------------------------------------------- /tests/snapshots/org.example.civixsnapshot-v22.06.0-entity3/original.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/totten/civix/b4c7e5fe832cb88a91b503a6f7ea436e7637f749/tests/snapshots/org.example.civixsnapshot-v22.06.0-entity3/original.zip -------------------------------------------------------------------------------- /tests/snapshots/org.example.civixsnapshot-v22.06.0-entity34/original.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/totten/civix/b4c7e5fe832cb88a91b503a6f7ea436e7637f749/tests/snapshots/org.example.civixsnapshot-v22.06.0-entity34/original.zip -------------------------------------------------------------------------------- /tests/snapshots/org.example.civixsnapshot-v22.06.0-kitchensink/original.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/totten/civix/b4c7e5fe832cb88a91b503a6f7ea436e7637f749/tests/snapshots/org.example.civixsnapshot-v22.06.0-kitchensink/original.zip -------------------------------------------------------------------------------- /tests/snapshots/org.example.civixsnapshot-v22.06.0-qf/original.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/totten/civix/b4c7e5fe832cb88a91b503a6f7ea436e7637f749/tests/snapshots/org.example.civixsnapshot-v22.06.0-qf/original.zip -------------------------------------------------------------------------------- /tests/snapshots/org.example.civixsnapshot-v22.10.2-empty/original.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/totten/civix/b4c7e5fe832cb88a91b503a6f7ea436e7637f749/tests/snapshots/org.example.civixsnapshot-v22.10.2-empty/original.zip -------------------------------------------------------------------------------- /tests/snapshots/org.example.civixsnapshot-v22.10.2-entity3/original.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/totten/civix/b4c7e5fe832cb88a91b503a6f7ea436e7637f749/tests/snapshots/org.example.civixsnapshot-v22.10.2-entity3/original.zip -------------------------------------------------------------------------------- /tests/snapshots/org.example.civixsnapshot-v22.10.2-entity34/original.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/totten/civix/b4c7e5fe832cb88a91b503a6f7ea436e7637f749/tests/snapshots/org.example.civixsnapshot-v22.10.2-entity34/original.zip -------------------------------------------------------------------------------- /tests/snapshots/org.example.civixsnapshot-v22.10.2-kitchensink/original.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/totten/civix/b4c7e5fe832cb88a91b503a6f7ea436e7637f749/tests/snapshots/org.example.civixsnapshot-v22.10.2-kitchensink/original.zip -------------------------------------------------------------------------------- /tests/snapshots/org.example.civixsnapshot-v22.10.2-qf/original.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/totten/civix/b4c7e5fe832cb88a91b503a6f7ea436e7637f749/tests/snapshots/org.example.civixsnapshot-v22.10.2-qf/original.zip -------------------------------------------------------------------------------- /tests/snapshots/org.example.civixsnapshot-v23.12.1-svc/original.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/totten/civix/b4c7e5fe832cb88a91b503a6f7ea436e7637f749/tests/snapshots/org.example.civixsnapshot-v23.12.1-svc/original.zip -------------------------------------------------------------------------------- /tests/snapshots/org.example.civixsnapshot-v23.12.2-entity34/original.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/totten/civix/b4c7e5fe832cb88a91b503a6f7ea436e7637f749/tests/snapshots/org.example.civixsnapshot-v23.12.2-entity34/original.zip -------------------------------------------------------------------------------- /tests/snapshots/org.example.civixsnapshot-v23.12.2-entity4/original.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/totten/civix/b4c7e5fe832cb88a91b503a6f7ea436e7637f749/tests/snapshots/org.example.civixsnapshot-v23.12.2-entity4/original.zip -------------------------------------------------------------------------------- /tests/snapshots/org.example.civixsnapshot-v24.09.1-entity4/original.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/totten/civix/b4c7e5fe832cb88a91b503a6f7ea436e7637f749/tests/snapshots/org.example.civixsnapshot-v24.09.1-entity4/original.zip -------------------------------------------------------------------------------- /upgrades/16.10.0.up.php: -------------------------------------------------------------------------------- 1 | <?php 2 | /** 3 | * As long as `CRM_*_Upgrader` classes are wired-up via lifecycle hooks (`hook_install`, etc), we 4 | * should include `hook_postInstall`. 5 | * 6 | * At some point in the future, this step could be removed if we configure `info.xml`'s `<upgrader>` option. 7 | */ 8 | return function (\CRM\CivixBundle\Generator $gen) { 9 | $io = \Civix::io(); 10 | 11 | if (!empty($gen->infoXml->get()->upgrader)) { 12 | $io->note("Found <upgrader> tag. Skip hook_postInstall."); 13 | return; 14 | } 15 | 16 | // Give a notice if the new `CRM/*/Upgrader/Base` has a substantive change. 17 | // Note: The change is actually done in the generic regen. This is just a notice. 18 | $phpBaseClass = \CRM\CivixBundle\Utils\Naming::createClassName($gen->infoXml->getNamespace(), 'Upgrader', 'Base'); 19 | $phpBaseFile = \CRM\CivixBundle\Utils\Naming::createClassFile($gen->infoXml->getNamespace(), 'Upgrader', 'Base'); 20 | if (file_exists($phpBaseFile)) { 21 | $content = file_get_contents($phpBaseFile); 22 | if (preg_match('|CRM_Core_BAO_Setting::setItem\(.revision, *.Extension.|', $content)) { 23 | $io->note([ 24 | "$phpBaseClass is based on a very old template.", 25 | "When $phpBaseClass is regenerated, it will transition an important data element from \"civicrm_setting\" to \"civicrm_extension\".", 26 | "Several extensions have made this transition, and there are no known issues.", 27 | "See also: https://issues.civicrm.org/jira/browse/CRM-19252", 28 | ]); 29 | if (!$io->confirm('Continue with upgrade?')) { 30 | throw new \RuntimeException('User stopped upgrade'); 31 | } 32 | } 33 | 34 | $gen->addHookDelegation('civicrm_postInstall', '', 35 | "This hook is important for supporting the new version of $phpBaseClass."); 36 | } 37 | else { 38 | $gen->addHookDelegation('civicrm_postInstall', '', 39 | 'If you use civix to facilitate database upgrades ("civix generate:upgrader"), then you should enable this stub. Otherwise, it is not needed.'); 40 | } 41 | 42 | }; 43 | -------------------------------------------------------------------------------- /upgrades/19.06.2.up.php: -------------------------------------------------------------------------------- 1 | <?php 2 | 3 | /** 4 | * The templates for PHPUnit tests have been updated to match a major 5 | * transition in PHPUnit -- *all upstream base-classes were renamed*: 6 | * 7 | * - `PHPUnit_Framework_TestCase` is the base-class in PHPUnit 4 and earlier 8 | * - `\PHPUnit\Framework\TestCase` is the base-class in PHPUnit 6 and later 9 | * - PHPUnit 5 is a transitional version which supports both naming conventions. 10 | * 11 | * In recent years, documentation+tooling in Civi have encouraged usage of 12 | * PHPUnit 5, so (hopefully) most environments are compatible with the newer naming. 13 | * 14 | * Going forward, `civix` will generate templates using the newer naming. 15 | * 16 | * To be consistent and forward-compatible, you should consider updating your 17 | * existing unit-tests to use the name base-classes. 18 | */ 19 | return function (\CRM\CivixBundle\Generator $gen) { 20 | /* @var \Symfony\Component\Console\Style\OutputStyle $io */ 21 | $io = \Civix::io(); 22 | 23 | $testFiles = \CRM\CivixBundle\Utils\Files::findFiles($gen->baseDir->string('tests'), '*.php'); 24 | $gen->updateTextFiles($testFiles, function(string $file, string $content) use ($io, $gen) { 25 | $old = 'PHPUnit_Framework_TestCase'; 26 | $new = 'PHPUnit\Framework\TestCase'; 27 | $relFile = \CRM\CivixBundle\Utils\Files::relativize($file, $gen->baseDir->string() . '/'); 28 | 29 | if (strpos($content, $old) === FALSE) { 30 | return $content; 31 | } 32 | 33 | $io->writeln("<info>PHPUnit 6.x+ changed the name of the standard base-class (</info>$old<info> => </info>$new<info>).</info>"); 34 | $io->writeln("<info>The file </info>$relFile<info> contains at least one reference to the old name.</info>"); 35 | $io->writeln("<info>The upgrader can do an automatic search-replace on this file.</info>"); 36 | if ($io->confirm("Perform search/replace?")) { 37 | $content = str_replace($old, $new, $content); 38 | } 39 | return $content; 40 | }); 41 | 42 | }; 43 | -------------------------------------------------------------------------------- /upgrades/20.06.0.up.php: -------------------------------------------------------------------------------- 1 | <?php 2 | 3 | /** 4 | * If you have a generated `phpunit.xml` or `phpunit.xml.dist` file, it may include the old option `syntaxCheck="false"`. 5 | * You can remove this. The option has been inert and will raise errors in newer versions of PHPUnit. 6 | */ 7 | return function (\CRM\CivixBundle\Generator $gen) { 8 | /* @var \Symfony\Component\Console\Style\OutputStyle $io */ 9 | $io = \Civix::io(); 10 | 11 | $files = array_filter([ 12 | $gen->baseDir->string('phpunit.xml'), 13 | $gen->baseDir->string('phpunit.xml.dist'), 14 | ], 'file_exists'); 15 | $gen->updateTextFiles($files, function(string $file, string $oldContent) use ($io, $gen) { 16 | $relFile = \CRM\CivixBundle\Utils\Files::relativize($file, $gen->baseDir->string() . '/'); 17 | 18 | $content = $oldContent; 19 | $content = preg_replace(';(\s+)syntaxCheck="[^\"]+">;', '>', $content); 20 | $content = preg_replace(';(\s+)syntaxCheck=\'[^\']+\'>;', '>', $content); 21 | $content = preg_replace(';(\s+)syntaxCheck="[^\"]+"(\s+);', '\1', $content); 22 | $content = preg_replace(';(\s+)syntaxCheck=\'[^\']+\'(\s+);', '\1', $content); 23 | if ($content !== $oldContent) { 24 | $io->writeln("$relFile<info> includes obsolete option </info>syntaxCheck=\"...\"<info>. Removing it.</info> "); 25 | } 26 | return $content; 27 | }); 28 | 29 | }; 30 | -------------------------------------------------------------------------------- /upgrades/22.05.0.up.php: -------------------------------------------------------------------------------- 1 | <?php 2 | 3 | return function (\CRM\CivixBundle\Generator $gen) { 4 | /* @var \Symfony\Component\Console\Style\OutputStyle $io */ 5 | $io = \Civix::io(); 6 | $prefix = $gen->infoXml->getFile(); 7 | 8 | $io->note([ 9 | "Civix v22.05 converts several functions to mixins. This reduces code-duplication and will enable easier updates in the future.", 10 | "The upgrader will examine your extension, remove old functions, and enable mixins (if needed).", 11 | "The following functions+mixins may be affected:\n\n" . implode("\n", [ 12 | "1. _*_civix_civicrm_angularModules() => ang-php@1.0.0", 13 | "2. _*_civix_civicrm_managed() => mgd-php@1.0.0", 14 | "3. _*_civix_civicrm_alterSettingsFolders() => setting-php@1.0.0", 15 | "4. _*_civix_civicrm_caseTypes() => case-xml@1.0.0", 16 | "5. _*_civix_civicrm_xmlMenu() => menu-xml@1.0.0", 17 | "6. _*_civix_civicrm_themes() => theme-php@1.0.0", 18 | ]), 19 | ]); 20 | 21 | if (!$io->confirm('Continue with upgrade?')) { 22 | throw new \RuntimeException('User stopped upgrade'); 23 | } 24 | 25 | // Do we need a mixin like `ang-php`? Maybe... check whether we have files like `*.ang.php`. 26 | $filePatterns = [ 27 | 'glob:ang/*.ang.php' => 'ang-php@1.0.0', 28 | 'find:*.mgd.php' => 'mgd-php@1.0.0', 29 | 'glob:settings/*.setting.php' => 'setting-php@1.0.0', 30 | 'glob:xml/case/*.xml' => 'case-xml@1.0.0', 31 | 'glob:xml/Menu/*.xml' => 'menu-xml@1.0.0', 32 | 'glob:*.theme.php' => 'theme-php@1.0.0', 33 | ]; 34 | $mixins = array_filter($filePatterns, 35 | function (string $mixin, string $pattern) use ($gen, $io) { 36 | $flagFiles = $gen->baseDir->search($pattern); 37 | $io->note($flagFiles 38 | ? "Enable \"$mixin\". There are files matching pattern \"$pattern\"." 39 | : "Skip \"$mixin\". There are no files matching pattern \"$pattern\"." 40 | ); 41 | return (bool) $flagFiles; 42 | }, 43 | ARRAY_FILTER_USE_BOTH 44 | ); 45 | $gen->addMixins($mixins); 46 | 47 | $gen->removeHookDelegation([ 48 | "_{$prefix}_civix_civicrm_angularModules", 49 | "_{$prefix}_civix_civicrm_managed", 50 | "_{$prefix}_civix_civicrm_alterSettingsFolders", 51 | "_{$prefix}_civix_civicrm_caseTypes", 52 | "_{$prefix}_civix_civicrm_xmlMenu", 53 | "_{$prefix}_civix_civicrm_themes", 54 | ]); 55 | 56 | }; 57 | -------------------------------------------------------------------------------- /upgrades/22.05.2.up.php: -------------------------------------------------------------------------------- 1 | <?php 2 | 3 | use CRM\CivixBundle\Builder\Mixins; 4 | use CRM\CivixBundle\Utils\EvilEx; 5 | 6 | return function (\CRM\CivixBundle\Generator $gen) { 7 | $mixins = new Mixins($gen->infoXml, $gen->baseDir->string('mixin')); 8 | $declared = $mixins->getDeclaredMixinConstraints(); 9 | $hasSettingMixin = (bool) preg_grep('/^setting-php@/', $declared); 10 | $action = NULL; 11 | 12 | $gen->updateModulePhp(function (\CRM\CivixBundle\Builder\Info $info, string $content) use ($gen, $hasSettingMixin, &$action) { 13 | $prefix = $gen->infoXml->getFile(); 14 | $hookFunc = "{$prefix}_civicrm_alterSettingsFolders"; 15 | $hookBody = [ 16 | 'static $configured = FALSE;', 17 | 'if ($configured) return;', 18 | '$configured = TRUE;', 19 | '$extRoot = dirname( __FILE__ ) . DIRECTORY_SEPARATOR;', 20 | '$extDir = $extRoot . \'settings\';', 21 | 'if(!in_array($extDir, $metaDataFolders)){', 22 | ' $metaDataFolders[] = $extDir;', 23 | '}', 24 | ]; 25 | 26 | $newContent = EvilEx::rewriteMultilineChunk($content, $hookBody, function(array $matchLines) use ($hookFunc, $content, $gen, $hasSettingMixin, &$action) { 27 | /* @var \Symfony\Component\Console\Style\OutputStyle $io */ 28 | $io = \Civix::io(); 29 | $matchLineKeys = array_keys($matchLines); 30 | $allLines = explode("\n", $content); 31 | $focusStart = min($matchLineKeys); 32 | $focusEnd = max($matchLineKeys); 33 | 34 | $io->note("The following chunk resembles an older template for \"{$hookFunc}()\"."); 35 | $gen->showCode($allLines, $focusStart - 4, $focusEnd + 4, $focusStart, $focusEnd); 36 | 37 | if ($hasSettingMixin) { 38 | $io->note([ 39 | "Similar functionality is now provided by the \"setting-php\" mixin, which is already enabled.", 40 | ]); 41 | } 42 | else { 43 | $io->note([ 44 | "Similar functionality is now available in the \"setting-php\" mixin, which is currently disabled.", 45 | ]); 46 | } 47 | 48 | $actions = [ 49 | 'm' => 'Use the mixin ("setting-php") and remove this boilerplate.', 50 | 'b' => 'Use the boilerplate and disable the mixin ("setting-php").', 51 | 'n' => 'Do nothing. Keep as-is. (You may manually change it later.)', 52 | ]; 53 | $action = $io->choice("What should we do?", $actions, 'm'); 54 | return ($action == 'm') ? [] : $matchLines; 55 | }); 56 | 57 | if ($action === 'm' && !$hasSettingMixin) { 58 | $gen->updateMixins(function (Mixins $mixins) { 59 | $mixins->addMixin('setting-php@1.0.0'); 60 | }); 61 | } 62 | elseif ($action === 'b' && $hasSettingMixin) { 63 | $gen->updateMixins(function (Mixins $mixins) { 64 | $mixins->removeMixin('setting-php'); 65 | }); 66 | } 67 | 68 | return $newContent; 69 | }); 70 | 71 | }; 72 | -------------------------------------------------------------------------------- /upgrades/22.10.0.up.php: -------------------------------------------------------------------------------- 1 | <?php 2 | 3 | /** 4 | * I haven't traced why, but it seems that core is getting more eager to scan 5 | * BAOs/DAOs early on -- in a way that provokes class-loading errors if your 6 | * extension defines entities and lacks `<psr0>` for `CRM_` 7 | * 8 | * Just add the '<psr0>` bit to everything. 9 | */ 10 | return function (\CRM\CivixBundle\Generator $gen) { 11 | 12 | $gen->updateInfo(function (\CRM\CivixBundle\Builder\Info $info) use ($gen) { 13 | /* @var \Symfony\Component\Console\Style\OutputStyle $io */ 14 | $io = \Civix::io(); 15 | 16 | $loaders = $info->getClassloaders(); 17 | $prefixes = array_column($loaders, 'prefix'); 18 | if (file_exists($gen->baseDir->string('CRM')) && !in_array('CRM_', $prefixes)) { 19 | $io->section('"CRM" Class-loader'); 20 | $io->note([ 21 | 'Older templates enabled class-loading via "hook_config" and "include_path".', 22 | 'Newer templates enable class-loading via "info.xml" ("<classloader>"). This fixes some edge-case issues and allows more configuration.', 23 | 'It is generally safe for these loaders to coexist. The upgrade will add "<classloader>" for the "CRM" folder.', 24 | ]); 25 | $io->warning([ 26 | 'If you use the rare (and ill-advised/unsupported) practice of class-overrides, then you may need extra diligence with any changes to class-loading.', 27 | ]); 28 | 29 | if (!$io->confirm('Continue with upgrade?')) { 30 | throw new \RuntimeException('User stopped upgrade'); 31 | } 32 | $loaders[] = ['type' => 'psr0', 'prefix' => 'CRM_', 'path' => '.']; 33 | $info->setClassLoaders($loaders); 34 | } 35 | 36 | }); 37 | 38 | }; 39 | -------------------------------------------------------------------------------- /upgrades/23.01.0.up.php: -------------------------------------------------------------------------------- 1 | <?php 2 | use CRM\CivixBundle\Utils\Formatting; 3 | 4 | return function (\CRM\CivixBundle\Generator $gen) { 5 | /* @var \Symfony\Component\Console\Style\OutputStyle $io */ 6 | $io = \Civix::io(); 7 | 8 | $previewChanges = []; 9 | $previewChanges[] = ['*_civix_civicrm_config', 'Remove Smarty boilerplate']; 10 | 11 | // Do we need a mixin like `ang-php`? Maybe... check whether we have files like `*.ang.php`. 12 | $filePatterns = [ 13 | 'find:*.tpl' => 'smarty-v2@1.0.0', 14 | ]; 15 | $mixins = array_filter($filePatterns, 16 | function (string $mixin, string $pattern) use ($gen, $io, &$previewChanges) { 17 | $flagFiles = $gen->baseDir->search($pattern); 18 | $previewChanges[] = [ 19 | 'info.xml', 20 | $flagFiles ? "Enable $mixin" : "Skip $mixin. (No files match \"$pattern\")", 21 | ]; 22 | return (bool) $flagFiles; 23 | }, 24 | ARRAY_FILTER_USE_BOTH 25 | ); 26 | 27 | $io->note([ 28 | "Civix v23.01 simplifies the boilerplate used for Smarty registration.", 29 | "The following may be affected:", 30 | Formatting::ol("%-31s %s\n", $previewChanges), 31 | ]); 32 | 33 | if (!empty($mixins)) { 34 | $io->warning([ 35 | "If Smarty templates are used by any lifecycle hooks (install,enable,disable,uninstall,upgrade,managed), then please re-test them.", 36 | ]); 37 | } 38 | 39 | if (!$io->confirm('Continue with upgrade?')) { 40 | throw new \RuntimeException('User stopped upgrade'); 41 | } 42 | 43 | $gen->addMixins($mixins); 44 | 45 | }; 46 | -------------------------------------------------------------------------------- /upgrades/23.02.0.up.php: -------------------------------------------------------------------------------- 1 | <?php 2 | 3 | /** 4 | * Upgrade hook_civicrm_entityTypes to use mixin 5 | */ 6 | return function (\CRM\CivixBundle\Generator $gen) { 7 | 8 | $prefix = $gen->infoXml->getFile(); 9 | 10 | if (is_dir($gen->baseDir->string('xml/schema/CRM'))) { 11 | \Civix::io()->note([ 12 | 'Civix 23.02 removes `*_civix_civicrm_entityTypes` in favor of a mixin `entity-types-php@1.0`.', 13 | 'This reduces code-duplication and will enable easier updates in the future.', 14 | 'This may raise the minimum requirements to CiviCRM v5.45.', 15 | ]); 16 | if (!\Civix::io()->confirm('Continue with upgrade?')) { 17 | throw new \RuntimeException('User stopped upgrade'); 18 | } 19 | 20 | $gen->addMixins(['entity-types-php@1.0']); 21 | } 22 | 23 | $gen->removeHookDelegation([ 24 | "_{$prefix}_civix_civicrm_entityTypes", 25 | ]); 26 | 27 | }; 28 | -------------------------------------------------------------------------------- /upgrades/23.02.1.up.php: -------------------------------------------------------------------------------- 1 | <?php 2 | 3 | /** 4 | * Circa v19.11.0, it became a default to add the psr4 rule for "Civi" folders. 5 | * However, as older extensions adopt newer technologies (like `Civi\Api4`), it helps 6 | * to add a similar to them. 7 | */ 8 | return function (\CRM\CivixBundle\Generator $gen) { 9 | 10 | $gen->updateInfo(function (\CRM\CivixBundle\Builder\Info $info) use ($gen) { 11 | /* @var \Symfony\Component\Console\Style\OutputStyle $io */ 12 | $io = \Civix::io(); 13 | 14 | $loaders = $info->getClassloaders(); 15 | $prefixes = array_column($loaders, 'prefix'); 16 | if (!in_array('Civi\\', $prefixes)) { 17 | $io->section('"Civi" Class-loader'); 18 | $io->note([ 19 | 'Technologies like APIv4 may require you to use the "Civi" namespace, which is not enabled on this extension.', 20 | 'You can automatically add the "Civi" namespace now to get prepared, or you can skip this.', 21 | '(If you change your mind later, then simply edit "info.xml" to add or remove "<classloader>" rules.)', 22 | ]); 23 | 24 | if ($io->confirm('Add the "Civi" namespace?')) { 25 | $loaders[] = ['type' => 'psr4', 'prefix' => 'Civi\\', 'path' => 'Civi']; 26 | $info->setClassLoaders($loaders); 27 | } 28 | } 29 | 30 | }); 31 | 32 | }; 33 | -------------------------------------------------------------------------------- /upgrades/24.09.0.up.php: -------------------------------------------------------------------------------- 1 | <?php 2 | 3 | use CRM\CivixBundle\Utils\Formatting; 4 | 5 | return function (\CRM\CivixBundle\Generator $gen) { 6 | 7 | // The logic to toggle 'pathload' and `civimix-schema@5` is actually 8 | // a general/recurring update in CRM\CivixBundle\Builder\Module::save(). 9 | // This merely serves as a notice that it will happen. 10 | 11 | $steps = []; 12 | if (!Civix::checker()->coreHasPathload()) { 13 | $steps[] = 'Create mixin/lib/pathload-0.php'; 14 | } 15 | if (!Civix::checker()->coreProvidesLibrary('civimix-schema@5')) { 16 | $steps[] = 'Create mixin/lib/' . basename($gen->mixinLibraries->available['civimix-schema@5']->file); 17 | } 18 | 19 | if (\Civix::checker()->hasUpgrader() && $steps) { 20 | \Civix::io()->note([ 21 | "This update adds a new helper, E::schema(), which requires the library civimix-schema@5. To enable support for older versions of CiviCRM (<5.73), the update will:\n\n" . Formatting::ol("%s\n", $steps), 22 | ]); 23 | } 24 | 25 | }; 26 | -------------------------------------------------------------------------------- /upgrades/25.01.0.up.php: -------------------------------------------------------------------------------- 1 | <?php 2 | 3 | use CRM\CivixBundle\Parse\PrimitiveFunctionVisitor; 4 | 5 | /** 6 | * If you have EFv2, then you probably don't want to implement hook_entityTypes directly. 7 | * 8 | * - After running 24.09.1, it creates '*.entityType.php' files and enables 9 | * entity-type-php@. You shouldn't need a manual declaration. 10 | * - Most existing implementations of hook_entityTypes do not provide all 11 | * the properties needed for EFv2. 12 | * - Registering twice (with incomplete data) is likely to cause trouble. 13 | * 14 | * This step will search for `hook_entityTypes` and warn if it's doing any non-trivial work. 15 | * It prompts you to (c)omment, (k)eep, or (d)elete the function. 16 | */ 17 | return function (\CRM\CivixBundle\Generator $gen) { 18 | 19 | if (!\Civix::checker()->hasSchemaPhp() && !\Civix::checker()->hasMixin('/^entity-types-php@2/')) { 20 | // OK, not our problem. 21 | return; 22 | } 23 | 24 | $gen->updateModulePhp(function (\CRM\CivixBundle\Builder\Info $infoXml, string $body) { 25 | $hookFunc = $infoXml->getFile() . '_civicrm_entityTypes'; 26 | $hookDelegate = '_' . $infoXml->getFile() . '_civix_civicrm_entityTypes'; 27 | 28 | /** 29 | * @param string $func 30 | * Ex: 'myfile_civicrm_fooBar' 31 | * @param string $sig 32 | * Ex: 'array &$arg1, string $arg2' 33 | * @param string $code 34 | * Ex: 'echo "Hello";\necho "World";' 35 | */ 36 | return PrimitiveFunctionVisitor::visit($body, function (string &$func, string &$sig, string &$code) use ($infoXml, $hookFunc, $hookDelegate) { 37 | if ($func !== $hookFunc || empty($code)) { 38 | return NULL; 39 | } 40 | 41 | $lines = preg_split(';\n\w*;', $code); 42 | $lines = array_map('trim', $lines); 43 | $lines = array_filter($lines, function ($line) { 44 | return !empty($line) && !preg_match(';^(#|//);', $line); 45 | }); 46 | $delegateRE = '/' . preg_quote($hookDelegate, '/') . '/i'; 47 | 48 | // $hasDelegate = !empty(preg_grep($delegateRE, $lines)); 49 | $hasBespokeLogic = !empty(preg_grep($delegateRE, $lines, PREG_GREP_INVERT)); 50 | 51 | if (!$hasBespokeLogic) { 52 | return NULL; 53 | } 54 | 55 | $io = Civix::io(); 56 | $io->title("Suspicious Entity Hook"); 57 | 58 | $fullCode = sprintf("function %s(%s) {%s}", $func, $sig, $code); 59 | Civix::generator()->showCode(explode("\n", $fullCode)); 60 | 61 | $io->note([ 62 | "In Entity Framework v2, conventions for metadata have changed.", 63 | "Metadata is usually loaded from schema/*.entityType.php.", 64 | "This implementation of hook_entityTypes contains special declarations. Consider whether this is needed.", 65 | "For example:\n - This function might duplicate the content of schema/*.entityType.php. Remove it to avoid conflicts.\n - This function might define special, dynamic entities. Keep it ensure continued operation.", 66 | ]); 67 | 68 | $actionLabels = [ 69 | 'c' => 'Comment-out this function', 70 | 'k' => 'Keep this function', 71 | 'd' => 'Delete this function', 72 | ]; 73 | $action = $io->choice("What should we do with {$func}()?", $actionLabels, 'c'); 74 | $results = ['k' => NULL, 'c' => 'COMMENT', 'd' => 'DELETE']; 75 | return $results[$action] ?? NULL; 76 | }); 77 | }); 78 | 79 | }; 80 | -------------------------------------------------------------------------------- /upgrades/25.01.1.up.php: -------------------------------------------------------------------------------- 1 | <?php 2 | 3 | use CRM\CivixBundle\Utils\MixinLibraries; 4 | 5 | /** 6 | * Earlier civix revisions would generate `civimix-schema@X.X.X.phar`. 7 | * This has compatibility issues with Backdrop. 8 | * Convert to `civimix-schema@X.X.X/`. 9 | */ 10 | return function (\CRM\CivixBundle\Generator $gen) { 11 | 12 | if ($gen->baseDir->search('glob:mixin/lib/civimix-schema@5.*.phar')) { 13 | $io = Civix::io(); 14 | $io->title("Convert civimix-schema@5"); 15 | 16 | $gen->updateMixinLibraries(function (MixinLibraries $mixinLibraries): void { 17 | $mixinLibraries->remove('civimix-schema@5'); 18 | $mixinLibraries->add('civimix-schema@5'); 19 | }); 20 | } 21 | 22 | }; 23 | --------------------------------------------------------------------------------