├── components ├── require.css ├── require.config.js └── typeaheadjs │ └── typeaheadjs-built.js ├── resources └── message.php ├── kubernetes ├── .htaccess ├── ingress.yaml ├── mongo.yaml ├── elasticsearch.yaml └── web.yaml ├── src ├── views │ ├── src │ │ └── less │ │ │ ├── view-home.less │ │ │ ├── view-result.less │ │ │ ├── global.less │ │ │ ├── variables.less │ │ │ └── style.less │ ├── css │ │ ├── images │ │ │ ├── logo.png │ │ │ ├── mouf.png │ │ │ ├── tcm.png │ │ │ ├── arrow.png │ │ │ ├── bg-head.jpg │ │ │ ├── bg-logo.png │ │ │ ├── favicon.ico │ │ │ ├── github.png │ │ │ ├── ico-dna.png │ │ │ ├── int_obj.png │ │ │ ├── twitter.png │ │ │ ├── class_obj.png │ │ │ ├── ico-code.png │ │ │ ├── ico-talk.png │ │ │ ├── ico-work.png │ │ │ ├── trait_obj.png │ │ │ ├── github-small.png │ │ │ ├── ico-search.png │ │ │ └── package_obj.png │ │ └── styles.css │ ├── classAnalyzer │ │ ├── 404.twig │ │ ├── classAnalyzer.js │ │ └── index.twig │ ├── packageAnalyzer │ │ ├── 404.twig │ │ └── index.twig │ ├── root │ │ ├── logo.twig │ │ ├── search.twig │ │ └── index.php │ ├── package.json │ └── Gruntfile.js ├── templates │ └── Mouf │ │ ├── Html │ │ └── Template │ │ │ ├── Menus │ │ │ ├── BootstrapMenu.twig │ │ │ └── BootstrapNavBar.twig │ │ │ └── BootstrapTemplate.twig │ │ └── Packanalyst │ │ └── Widgets │ │ ├── SearchBlock.twig │ │ ├── Node.twig │ │ └── Node__revert.twig └── Mouf │ └── Packanalyst │ ├── Widgets │ ├── SearchBlock.php │ ├── Graph.php │ └── Node.php │ ├── Command │ ├── GetPackagistScoresCommand.php │ ├── ReindexCommand.php │ ├── ForceRefreshCommand.php │ ├── ResetCommand.php │ └── RunCommand.php │ ├── Services │ ├── PackagistScoreService.php │ ├── ComposerSrcDirectoryFinder.php │ ├── StoreInDbNodeVisitor.php │ ├── ElasticSearchService.php │ └── FetchDataService.php │ ├── Controllers │ ├── PackageAnalyzerController.php │ ├── RootController.php │ └── ClassAnalyzerController.php │ ├── ClassesDetector.php │ └── Dao │ ├── PackageDao.php │ └── ItemDao.php ├── tests ├── bootstrap.php └── Packanalyst │ └── Services │ └── ElasticSearchServiceTest.php ├── vendor-bin └── phpstan │ └── composer.json ├── .gitignore ├── phpstan.neon ├── .dockerignore ├── mouf ├── Mouf.php ├── MoufUI.php └── installs_app.php ├── aenthill.json ├── phpunit.xml.dist ├── console.php ├── Dockerfile ├── test.php ├── .env ├── composer.json ├── docker-compose.yml ├── .htaccess ├── config.php ├── .travis.yml └── README.md /components/require.css: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /resources/message.php: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /kubernetes/.htaccess: -------------------------------------------------------------------------------- 1 | order deny,allow 2 | deny from all 3 | -------------------------------------------------------------------------------- /src/views/src/less/view-home.less: -------------------------------------------------------------------------------- 1 | .jumbotron { 2 | background: none; 3 | } -------------------------------------------------------------------------------- /tests/bootstrap.php: -------------------------------------------------------------------------------- 1 | Analyzing class {{ class }} 2 | 3 |
Sorry, couldn't find class {{ class }}.
-------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *~ 2 | vendor 3 | downloads/* 4 | .buildpath 5 | .composer 6 | .project 7 | .settings 8 | mouf/no_commit/* 9 | src/views/node_modules/* 10 | .idea/* 11 | .bowerrc 12 | bower.json 13 | -------------------------------------------------------------------------------- /phpstan.neon: -------------------------------------------------------------------------------- 1 | parameters: 2 | ignoreErrors: 3 | - "#on an unknown class Mouf#" 4 | - "#Constant .* not found#" 5 | #includes: 6 | # - vendor/thecodingmachine/phpstan-strict-rules/phpstan-strict-rules.neon -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | Dockerfile 2 | 3 | *~ 4 | vendor 5 | downloads/* 6 | .buildpath 7 | .composer 8 | .project 9 | .settings 10 | mouf/no_commit/* 11 | src/views/node_modules/* 12 | .idea/* 13 | .bowerrc 14 | bower.json 15 | /kubernetes -------------------------------------------------------------------------------- /src/views/classAnalyzer/classAnalyzer.js: -------------------------------------------------------------------------------- 1 | $(function() { 2 | $('a.otherpackageslink').click(function() { 3 | $(this).parent('.otherpackagescontainer').find('.otherpackages').show(); 4 | $(this).hide(); 5 | return false; 6 | }); 7 | }); -------------------------------------------------------------------------------- /src/views/packageAnalyzer/404.twig: -------------------------------------------------------------------------------- 1 |

Analyzing package {{ packageName }}{% if (packageVersion) %} ({{ packageVersion }}){% endif %}

2 | 3 |
Sorry, couldn't find package {{ packageName }}{% if (packageVersion) %} ({{ packageVersion }}){% endif %}.
-------------------------------------------------------------------------------- /mouf/Mouf.php: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/views/root/logo.twig: -------------------------------------------------------------------------------- 1 |

2 | 3 | Logo Packanalyst 4 | 5 | Explore PHP classes from Packagist 6 | 7 |

-------------------------------------------------------------------------------- /src/views/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "packanalyst", 3 | "version": "0.1.0", 4 | "devDependencies": { 5 | "bower": "~1.3.5", 6 | "grunt": "^0.4.5", 7 | "grunt-contrib-less": "~0.11.3", 8 | "grunt-contrib-watch": "~0.6.1", 9 | "grunt-modernizr": "~0.5.2", 10 | "load-grunt-tasks": "~0.6.0", 11 | "time-grunt": "~0.3.2" 12 | 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/templates/Mouf/Html/Template/Menus/BootstrapMenu.twig: -------------------------------------------------------------------------------- 1 | {% if (not this.hidden) and children %} 2 | 7 | {% endif %} -------------------------------------------------------------------------------- /components/require.config.js: -------------------------------------------------------------------------------- 1 | var components = { 2 | "packages": [ 3 | { 4 | "name": "typeaheadjs", 5 | "main": "typeaheadjs-built.js" 6 | } 7 | ], 8 | "baseUrl": "components" 9 | }; 10 | if (typeof require !== "undefined" && require.config) { 11 | require.config(components); 12 | } else { 13 | var require = components; 14 | } 15 | if (typeof exports !== "undefined" && typeof module !== "undefined") { 16 | module.exports = components; 17 | } -------------------------------------------------------------------------------- /aenthill.json: -------------------------------------------------------------------------------- 1 | { 2 | "aents": [ 3 | { 4 | "image": "theaentmachine/aent-docker-compose:snapshot", 5 | "handled_events": [ 6 | "ADD", 7 | "REMOVE", 8 | "NEW_DOCKER_SERVICE_INFO", 9 | "DELETE_DOCKER_SERVICE" 10 | ] 11 | }, 12 | { 13 | "image": "theaentmachine/aent-traefik:snapshot", 14 | "handled_events": [ 15 | "ADD", 16 | "NEW_VIRTUAL_HOST" 17 | ] 18 | }, 19 | { 20 | "image": "theaentmachine/aent-php:snapshot", 21 | "handled_events": [ 22 | "ADD" 23 | ] 24 | } 25 | ] 26 | } -------------------------------------------------------------------------------- /kubernetes/ingress.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: "extensions/v1beta1" 3 | kind: Ingress 4 | metadata: 5 | name: web-ingress 6 | annotations: 7 | ingress.kubernetes.io/ssl-redirect: "true" 8 | kubernetes.io/ingress.class: nginx 9 | #kubernetes.io/tls-acme: 'true' 10 | certmanager.k8s.io/cluster-issuer: letsencrypt-prod-cluster-issuer 11 | spec: 12 | tls: 13 | - hosts: 14 | - packanalyst.com 15 | secretName: tls-certificate 16 | rules: 17 | - host: packanalyst.com 18 | http: 19 | paths: 20 | - backend: 21 | serviceName: web 22 | servicePort: 80 23 | -------------------------------------------------------------------------------- /phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 14 | 15 | 16 | ./tests/Packanalyst/Services/ 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /console.php: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env php 2 | add(new RunCommand()); 14 | $application->add(new ReindexCommand()); 15 | $application->add(new ResetCommand()); 16 | $application->add(new GetPackagistScoresCommand()); 17 | $application->add(new ForceRefreshCommand()); 18 | $application->run(); 19 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM thecodingmachine/php:7.2-v2-apache-node8 2 | 3 | ENV PHP_EXTENSION_MONGODB=1 4 | ENV PHP_INI_MEMORY_LIMIT=512M 5 | 6 | ENV CRON_USER_1=docker 7 | ENV CRON_SCHEDULE_1="*/5 * * * *" 8 | ENV CRON_COMMAND_1="cd /var/www/html;ulimit -S -s 131072;./console.php run -v" 9 | 10 | ENV CRON_USER_2=docker 11 | ENV CRON_SCHEDULE_2="0 5 * * *" 12 | ENV CRON_COMMAND_2="cd /var/www/html;./console.php get-scores -v" 13 | 14 | # Delete files older than 30 days 15 | ENV CRON_USER_3=docker 16 | ENV CRON_SCHEDULE_3="23 0 * * *" 17 | ENV CRON_COMMAND_3="find /var/downloads/ -maxdepth 2 -type d -mtime +30 | xargs rm -rf" 18 | 19 | COPY --chown=docker:docker . . 20 | 21 | RUN composer install 22 | RUN cd src/views/ && npm install 23 | -------------------------------------------------------------------------------- /test.php: -------------------------------------------------------------------------------- 1 | getIO()) 13 | )); 14 | */ 15 | 16 | $package = new Package('test/test', '1.0.0', '1.0.0'); 17 | $package->setReleaseDate(new DateTime()); 18 | 19 | Mouf::getPackageVersionRepository()->findOrCreatePackageVersion($package); 20 | //Mouf::getPackageRepository()->findOrCreatePackage('mouf/mouf'); 21 | Mouf::getNeo4jEntityManager()->flush(); -------------------------------------------------------------------------------- /src/Mouf/Packanalyst/Widgets/SearchBlock.php: -------------------------------------------------------------------------------- 1 | search = $search; 25 | } 26 | 27 | /** 28 | * The text displayed into the search block. 29 | * 30 | * @param string $search 31 | */ 32 | public function setSearch($search) 33 | { 34 | $this->search = $search; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /.env: -------------------------------------------------------------------------------- 1 | # The host name for the Elastic Search server 2 | ELASTICSEARCH_HOST=elasticsearch 3 | 4 | # The default port to connect to Elastic Search server 5 | ELASTICSEARCH_PORT=9200 6 | 7 | # A random string. It should be different for any application deployed. 8 | SECRET=HLxRssObAZpJdFYfHJpT 9 | 10 | # The download directory 11 | DOWNLOAD_DIR=/var/downloads 12 | 13 | # Connection string to MongoDB 14 | MONGODB_CONNECTIONSTRING=mongodb://mongo:27017 15 | 16 | # Your Google Analytics key. Leave empty if you want to disable Google Analytics tracking. Don't have a key for your website? Get one here: http://www.google.com/analytics/ 17 | GOOGLE_ANALYTICS_KEY= 18 | 19 | # The base domain name to track (if you are tracking sub-domains). In the form: '.example.com'. Keep this empty if you don't track subdomains. 20 | GOOGLE_ANALYTICS_DOMAIN_NAME= 21 | 22 | # Set to true to enable debug/development mode. 23 | DEBUG=true 24 | -------------------------------------------------------------------------------- /src/Mouf/Packanalyst/Command/GetPackagistScoresCommand.php: -------------------------------------------------------------------------------- 1 | setName('get-scores') 18 | ->setDescription('Retrieve scores of all packages on Packagist.') 19 | ->setHelp(<<get-scores command loads all packages scores from Packagist. 21 | EOT 22 | ) 23 | ; 24 | } 25 | 26 | protected function execute(InputInterface $input, OutputInterface $output) 27 | { 28 | \Mouf::getPackagistStatsLock()->acquireLock(); 29 | $packagistScoreService = \Mouf::getPackagistScoreService(); 30 | $packagistScoreService->updateAllScores(); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/Mouf/Packanalyst/Command/ReindexCommand.php: -------------------------------------------------------------------------------- 1 | setName('reindex-el') 22 | ->setDescription('Reindexes all elastic search records.') 23 | ->setHelp(<<reindex-el command reindexes all elastic search records from the Neo4J database. 25 | EOT 26 | ) 27 | ; 28 | } 29 | 30 | protected function execute(InputInterface $input, OutputInterface $output) 31 | { 32 | $elasticSearchService = \Mouf::getElasticSearchService(); 33 | $elasticSearchService->reindexAll(); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /mouf/MoufUI.php: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/Mouf/Packanalyst/Command/ForceRefreshCommand.php: -------------------------------------------------------------------------------- 1 | setName('force-refresh') 22 | ->setDescription('Force refreshing all packages from packages.') 23 | ->setHelp(<<force-refresh command sets a flag on all packages for refreshing them. 25 | EOT 26 | ) 27 | ; 28 | } 29 | 30 | protected function execute(InputInterface $input, OutputInterface $output) 31 | { 32 | $itemDao = \Mouf::getItemDao(); 33 | $packageDao = \Mouf::getPackageDao(); 34 | $itemDao->createIndex(); 35 | $packageDao->createIndex(); 36 | 37 | $packageDao->refreshAllPackages(); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/templates/Mouf/Html/Template/Menus/BootstrapNavBar.twig: -------------------------------------------------------------------------------- 1 | {% if not this.allWidth %} 2 |
3 | {% endif %} 4 | 29 | {% if not this.allWidth %} 30 |
31 | {% endif %} -------------------------------------------------------------------------------- /src/views/root/search.twig: -------------------------------------------------------------------------------- 1 | 4 | 5 | {{ totalCount }} results found: 6 | 7 |
    8 | {% for item in searchResults %} 9 | 12 | {% else %} 13 |
  • No class / interface / trait / package with name "{{ query }}"!
  • 14 | {% endfor %} 15 |
16 | 17 | 18 | {% if nbPages != 0 %} 19 |
    20 | 21 | {% if page != 0 %} 22 |
  • «
  • 23 | {% endif %} 24 | 25 | {% for i in 0..nbPages %} 26 |
  • 27 | {{ i+1 }} 28 |
  • 29 | {% endfor %} 30 | 31 | {% if page != nbPages %} 32 |
  • »
  • 33 | {% endif %} 34 | 35 |
36 | {% endif %} 37 | -------------------------------------------------------------------------------- /src/views/Gruntfile.js: -------------------------------------------------------------------------------- 1 | module.exports = function(grunt) { 2 | require('load-grunt-tasks')(grunt); 3 | require('time-grunt')(grunt); 4 | 5 | grunt.initConfig({ 6 | pkg: grunt.file.readJSON('package.json'), 7 | less: { 8 | dev: { 9 | files: { 10 | 'css/styles.css': [ 11 | 'src/less/style.less' 12 | ] 13 | } 14 | }, 15 | build: { 16 | files: { 17 | 'css/styles.min.css': [ 18 | 'src/less/style.less' 19 | ] 20 | }, 21 | options: { 22 | compress: true 23 | } 24 | } 25 | }, 26 | watch: { 27 | less: { 28 | tasks: ['less:dev', 'autoprefixer:dev'] 29 | } 30 | } 31 | }); 32 | 33 | 34 | grunt.loadNpmTasks('grunt-contrib-watch'); 35 | 36 | grunt.registerTask('default', [ 37 | 'dev' 38 | ]); 39 | 40 | grunt.registerTask('dev', [ 41 | 'less:dev' 42 | ]); 43 | 44 | grunt.registerTask('build', [ 45 | 'less:build' 46 | ]); 47 | }; -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "require" : { 3 | "composer/composer" : "~1.0.0", 4 | "nikic/php-parser" : "~4.0", 5 | "mouf/mouf" : "~2.0", 6 | "mouf/utils.common.doctrine-annotations-wrapper" : "~1.2", 7 | "elasticsearch/elasticsearch" : "~1.1", 8 | "mouf/mvc.splash" : "~5.0", 9 | "koala-framework/composer-extra-assets": "~1.1", 10 | "geoffroy-aubry/logger" : "1.*", 11 | "michelf/php-markdown" : "~1.4", 12 | "ezyang/htmlpurifier" : "~4.6", 13 | "guzzlehttp/guzzle" : "^6", 14 | "mouf/utils.common.lock" : "dev-master", 15 | "mouf/modules.google-analytics" : "~4.0", 16 | "mongodb/mongodb": "^1.0.0@beta" 17 | }, 18 | "require-dev": { 19 | "phpunit/phpunit" : "^4", 20 | "bamarni/composer-bin-plugin": "^1.2" 21 | }, 22 | "autoload" : { 23 | "psr-4" : { 24 | "Mouf\\Packanalyst\\" : "src/Mouf/Packanalyst" 25 | } 26 | }, 27 | "minimum-stability" : "dev", 28 | "prefer-stable": true, 29 | "scripts": { 30 | "phpstan": "phpstan analyse src -c phpstan.neon --level=2 --no-progress -vvv", 31 | "post-install-cmd": ["@composer bin all install --ansi"], 32 | "post-update-cmd": ["@composer bin all update --ansi"] 33 | }, 34 | "extra" : { 35 | "require-bower": { 36 | "jquery": "~1.11", 37 | "typeahead.js": "0.10.*" 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/views/packageAnalyzer/index.twig: -------------------------------------------------------------------------------- 1 | 4 | 5 | {% if package.downloads %} 6 |

7 | Downloads: {{ package.downloads }} 8 |

9 | {% endif %} 10 | 11 | {% if package.favers %} 12 |

13 | Favorites: {{ package.favers }} 14 |

15 | {% endif %} 16 | 17 | 18 |
{{ package.description }}
19 | 20 | {% if otherVersions %} 21 |

22 | Other packages versions: 23 | {% for otherVersion in otherVersions %} 24 | {{ otherVersion }} 25 | {% endfor %} 26 |

27 | {% endif %} 28 | 29 |

View on Packagist

30 | 31 |

Declared items in this package

32 | 33 |
    34 | {% for item in itemsList %} 35 |
    36 | 37 |
    38 | {% else %} 39 |
  • No class / interface / trait found in this package!
  • 40 | {% endfor %} 41 |
-------------------------------------------------------------------------------- /kubernetes/mongo.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: "extensions/v1beta1" 3 | kind: "Deployment" 4 | metadata: 5 | name: "mongo" 6 | labels: 7 | app: "mongo" 8 | spec: 9 | replicas: 1 10 | strategy: 11 | type: Recreate 12 | rollingUpdate: ~ 13 | selector: 14 | matchLabels: 15 | app: "mongo" 16 | template: 17 | metadata: 18 | labels: 19 | app: "mongo" 20 | spec: 21 | containers: 22 | - name: "mongodb" 23 | image: "mongo:3.6" 24 | volumeMounts: 25 | - name: mongo-data 26 | mountPath: /data/db 27 | resources: 28 | requests: 29 | memory: "2G" 30 | cpu: "250m" 31 | limits: 32 | memory: "8G" 33 | cpu: "2" 34 | volumes: 35 | - name: mongo-data 36 | persistentVolumeClaim: 37 | claimName: mongo-claim 38 | --- 39 | apiVersion: v1 40 | kind: PersistentVolumeClaim 41 | metadata: 42 | name: mongo-claim 43 | spec: 44 | accessModes: 45 | - ReadWriteOnce 46 | resources: 47 | requests: 48 | storage: 50Gi 49 | --- 50 | apiVersion: v1 51 | kind: Service 52 | metadata: 53 | name: mongo 54 | spec: 55 | selector: 56 | app: "mongo" 57 | ports: 58 | - name: mongo 59 | port: 27017 60 | targetPort: 27017 61 | -------------------------------------------------------------------------------- /src/templates/Mouf/Packanalyst/Widgets/SearchBlock.twig: -------------------------------------------------------------------------------- 1 | 29 | 30 | 40 | -------------------------------------------------------------------------------- /kubernetes/elasticsearch.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: "extensions/v1beta1" 3 | kind: "Deployment" 4 | metadata: 5 | name: "elasticsearch" 6 | labels: 7 | app: "elasticsearch" 8 | spec: 9 | replicas: 1 10 | strategy: 11 | type: Recreate 12 | rollingUpdate: ~ 13 | selector: 14 | matchLabels: 15 | app: "elasticsearch" 16 | template: 17 | metadata: 18 | labels: 19 | app: "elasticsearch" 20 | spec: 21 | containers: 22 | - name: "elasticsearch" 23 | image: "elasticsearch:2.2" 24 | volumeMounts: 25 | - name: elasticsearch-data 26 | mountPath: /usr/share/elasticsearch/data 27 | resources: 28 | requests: 29 | memory: "2G" 30 | cpu: "250m" 31 | limits: 32 | memory: "4G" 33 | cpu: "2" 34 | volumes: 35 | - name: elasticsearch-data 36 | persistentVolumeClaim: 37 | claimName: elasticsearch-claim 38 | --- 39 | apiVersion: v1 40 | kind: PersistentVolumeClaim 41 | metadata: 42 | name: elasticsearch-claim 43 | spec: 44 | accessModes: 45 | - ReadWriteOnce 46 | resources: 47 | requests: 48 | storage: 10Gi 49 | --- 50 | apiVersion: v1 51 | kind: Service 52 | metadata: 53 | name: elasticsearch 54 | spec: 55 | selector: 56 | app: "elasticsearch" 57 | ports: 58 | - name: elasticsearch 59 | port: 9200 60 | targetPort: 9200 61 | -------------------------------------------------------------------------------- /src/Mouf/Packanalyst/Command/ResetCommand.php: -------------------------------------------------------------------------------- 1 | setName('reset') 22 | ->setDescription('Deletes all data.') 23 | ->setHelp(<<reset command deletes all data from the MongoDB and the ElasticSearch database. Use with caution! It also creates the MongoDB collections with the indexes. 25 | EOT 26 | ) 27 | ; 28 | } 29 | 30 | protected function execute(InputInterface $input, OutputInterface $output) 31 | { 32 | $itemDao = \Mouf::getItemDao(); 33 | $packageDao = \Mouf::getPackageDao(); 34 | $itemDao->drop(); 35 | $itemDao->createIndex(); 36 | $packageDao->drop(); 37 | $packageDao->createIndex(); 38 | 39 | //$fetchDataService = \Mouf::getFetchDataService(); 40 | //$fetchDataService->reset(); 41 | $elasticSearchService = \Mouf::getElasticSearchService(); 42 | $elasticSearchService->deleteIndex(); 43 | $elasticSearchService->createIndex(); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/views/src/less/view-result.less: -------------------------------------------------------------------------------- 1 | 2 | .item .stars-container { 3 | float: right; 4 | padding: 3px; 5 | } 6 | 7 | .pad { 8 | padding-left: 40px; 9 | } 10 | 11 | .pad.arrow { 12 | background: url("images/arrow.png") no-repeat; 13 | background-position: 20px 8px; 14 | position: relative; 15 | &:before { 16 | content: ""; 17 | position: absolute; 18 | width: 0px; 19 | height: 100%; 20 | left: 20px; 21 | border-left: 1px dashed #100; 22 | } 23 | &:last-child { 24 | &:before { 25 | height: 20px; 26 | } 27 | } 28 | } 29 | 30 | #content > .pad.arrow { 31 | &:last-child { 32 | &:before { 33 | height: 20px; 34 | } 35 | } 36 | } 37 | 38 | .highlight { 39 | background-color: #ffff99; 40 | } 41 | 42 | ul.classgraph { 43 | padding-left: 0px; 44 | list-style-type: none; 45 | & > li ul li { 46 | position: relative; 47 | &:before { 48 | content: ""; 49 | position: absolute; 50 | width: 0px; 51 | height: 100%; 52 | left: -20px; 53 | border-left: 1px dashed #100; 54 | } 55 | &:last-child { 56 | &:before { 57 | height: 20px; 58 | } 59 | } 60 | } 61 | } 62 | 63 | ul.classgraph ul { 64 | list-style-image: url("images/arrow.png"); 65 | } 66 | 67 | .githublink { 68 | .transition(); 69 | padding-left: 40px; 70 | background-image: url("images/github-small.png"); 71 | background-position: 5px center; 72 | background-repeat: no-repeat; 73 | min-height: 20px; 74 | display: inline-block; 75 | border-radius: 0; 76 | border-color: @darkColor; 77 | } -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.3' 2 | services: 3 | app: 4 | image: thecodingmachine/php:7.2-v1-apache-node6 5 | env_file: 6 | - .env 7 | environment: 8 | PHP_EXTENSION_MONGODB: '1' 9 | PHP_EXTENSION_XDEBUG: 1 10 | PHP_INI_MEMORY_LIMIT: 256M 11 | PHP_IDE_CONFIG: "serverName=Docker" 12 | XDEBUG_CONFIG: "remote_autostart=1" 13 | PHP_INI_XDEBUG__MAX_NESTING_LEVEL: 2000 14 | STARTUP_COMMAND_1: composer install 15 | STARTUP_COMMAND_2: cd src/views/ && npm install 16 | volumes: 17 | - type: bind 18 | source: ./. 19 | target: /var/www/html 20 | read_only: false 21 | - type: volume 22 | source: downloads 23 | target: /var/downloads 24 | read_only: false 25 | labels: 26 | traefik.enable: 'true' 27 | traefik.backend: app 28 | traefik.frontend.rule: Host:packanalyst.localhost 29 | traefik.port: '80' 30 | traefik: 31 | image: traefik:1.6 32 | command: 33 | - --docker 34 | - --docker.exposedbydefault=false 35 | ports: 36 | - 80:80 37 | volumes: 38 | - type: bind 39 | source: /var/run/docker.sock 40 | target: /var/run/docker.sock 41 | read_only: false 42 | mongo: 43 | image: mongo:3.6 44 | volumes: 45 | - type: volume 46 | source: mongodata 47 | target: /data/db 48 | read_only: false 49 | elasticsearch: 50 | image: elasticsearch:2.2 51 | volumes: 52 | - type: volume 53 | source: elasticsearchdata 54 | target: /usr/share/elasticsearch/data 55 | read_only: false 56 | 57 | volumes: 58 | downloads: 59 | mongodata: 60 | elasticsearchdata: -------------------------------------------------------------------------------- /.htaccess: -------------------------------------------------------------------------------- 1 | 2 | # Use an error page as index file. It makes sure a proper error is displayed if 3 | # mod_rewrite is not available. Additionally, this reduces the matching process for the 4 | # start page (path "/") because otherwise Apache will apply the rewriting rules 5 | # to each configured DirectoryIndex file (e.g. index.php, index.html, index.pl). 6 | DirectoryIndex vendor/mouf/mvc.splash/src/rewrite_missing.php 7 | 8 | 9 | 10 | RewriteEngine On 11 | 12 | # .htaccess RewriteBase related tips courtesy of Symfony 2's skeleton app. 13 | 14 | # Determine the RewriteBase automatically and set it as environment variable. 15 | # If you are using Apache aliases to do mass virtual hosting or installed the 16 | # project in a subdirectory, the base path will be prepended to allow proper 17 | # resolution of the base directory and to redirect to the correct URI. It will 18 | # work in environments without path prefix as well, providing a safe, one-size 19 | # fits all solution. But as you do not need it in this case, you can comment 20 | # the following 2 lines to eliminate the overhead. 21 | RewriteCond %{REQUEST_URI}::$1 ^(/.+)/(.*)::\2$ 22 | RewriteRule ^(.*) - [E=BASE:%1] 23 | 24 | # If the requested filename exists, and has an allowed extension, simply serve it. 25 | # We only want to let Apache serve files and not directories. 26 | RewriteCond %{REQUEST_FILENAME} -f 27 | RewriteRule .*((\.(js|ico|gif|jpg|png|css|woff|ttf|svg|eot|map)$)|^vendor) - [L] 28 | 29 | # Rewrite all other queries to the front controller. 30 | RewriteRule .? %{ENV:BASE}/vendor/mouf/mvc.splash/src/splash.php [L] 31 | -------------------------------------------------------------------------------- /src/templates/Mouf/Packanalyst/Widgets/Node.twig: -------------------------------------------------------------------------------- 1 |
  • 2 |
    3 |
    {{ name }} 4 | {% spaceless %} 5 |
    6 | {% if this.getNbStars() %} 7 | {% for i in 1..this.getNbStars() %} 8 | 9 | {% endfor %} 10 | {% endif %} 11 |
    12 | {% endspaceless %} 13 |
    14 | {% for packageName, versions in this.getImportantPackages() %} 15 |
    16 | {{ packageName }} 17 | 18 | {% for version in versions%} 19 | {{ version }} 20 | {% endfor %} 21 | 22 |
    23 | {% endfor %} 24 | 25 | {% if this.getNotImportantPackages() %} 26 |
    27 | View more packages 28 |
    29 | {% for packageName, versions in this.getNotImportantPackages() %} 30 |
    31 | {{ packageName }} 32 | 33 | {% for version in versions%} 34 | {{ version }} 35 | {% endfor %} 36 | 37 |
    38 | {% endfor %} 39 |
    40 |
    41 | {% endif %} 42 | 43 | 44 |
    45 | {% if children %} 46 |
      47 | {% for child in this.getChildrenSortedByScore() %} 48 | {{ toHtml(child) }} 49 | {% endfor %} 50 |
    51 | {% endif %} 52 |
  • 53 | -------------------------------------------------------------------------------- /config.php: -------------------------------------------------------------------------------- 1 | createIndex(); 13 | } catch (\Exception $e) { 14 | // Ignore if index already exist. 15 | } 16 | 17 | try { 18 | $elasticSearchService->deleteItemName('unique_test_case'); 19 | } catch (\Exception $e) { 20 | // Ignore if key does not exists. 21 | } 22 | 23 | $elasticSearchService->storeItemName('unique_test_case'); 24 | 25 | $results = $elasticSearchService->suggestItemName2('unique_test_case'); 26 | 27 | $this->assertEquals(1, $results['total']); 28 | $this->assertEquals(null, $results['hits'][0]['_source']['type']); 29 | 30 | $elasticSearchService->storeItemName('unique_test_case', 'class'); 31 | $results = $elasticSearchService->suggestItemName2('unique_test_case'); 32 | 33 | $this->assertEquals(1, $results['total']); 34 | $this->assertEquals('class', $results['hits'][0]['_source']['type']); 35 | 36 | // TODO: write test to test ID, update, and so on!!! 37 | // TODO: write test to test ID, update, and so on!!! 38 | // TODO: write test to test ID, update, and so on!!! 39 | // TODO: write test to test ID, update, and so on!!! 40 | // TODO: write test to test ID, update, and so on!!! 41 | // TODO: write test to test ID, update, and so on!!! 42 | // TODO: write test to test ID, update, and so on!!! 43 | // TODO: write test to test ID, update, and so on!!! 44 | // TODO: write test to test ID, update, and so on!!! 45 | $elasticSearchService->deleteItemName('unique_test_case'); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/views/classAnalyzer/index.twig: -------------------------------------------------------------------------------- 1 | 8 | 9 | {% if inheritLimit %} 10 |
    There are more than 10000 classes inheriting '{{ class }}'. 11 | Displaying first 10000 results.
    12 | {% endif %} 13 | 14 | {% if description %} 15 |

    Description

    16 |
    17 |
    {{ description | raw }}
    18 |
    19 | {% endif %} 20 | 21 | 22 |

    Type hierarchy

    23 | 24 | {{ inheritNodesHtml|raw }} 25 | 26 |

    {{ type | capitalize }} usage:

    27 | 28 | {% if usedInItems %} 29 | 30 | This {{ type }} is used in: 31 | 32 | 33 | 34 | 35 | 36 | 37 | {% for item in usedInItems %} 38 | 39 | 40 | 49 | 50 | {% endfor %} 51 | {% if usedInItems.count() == 1000 %} 52 | 53 | 54 | 55 | 56 | {% endif %} 57 |
    ClassPackage
    {{ item.name }} 41 |
    42 | {{ item.packageName }} 43 | 44 | {{ item.packageVersion }} 45 |
    46 |
    47 | 48 |
    And many more....
    58 | {% else %} 59 | This {{ type }} is not referred by any other class/interface/traits in packagist packages. 60 | {% endif %} 61 | -------------------------------------------------------------------------------- /src/Mouf/Packanalyst/Services/PackagistScoreService.php: -------------------------------------------------------------------------------- 1 | 13 | */ 14 | class PackagistScoreService 15 | { 16 | private $packageDao; 17 | private $logger; 18 | 19 | // Number of seconds to wait between requests 20 | const WAIT_TIME_BETWEEN_REQUESTS = 10; 21 | 22 | public function __construct(PackageDao $packageDao, LoggerInterface $logger) 23 | { 24 | $this->packageDao = $packageDao; 25 | $this->logger = $logger; 26 | } 27 | 28 | public function updateAllScores() 29 | { 30 | $i = 1; 31 | 32 | do { 33 | $this->logger->notice('Downloading scores for page {page}', ['page' => $i]); 34 | 35 | $result = $this->request($i); 36 | 37 | foreach ($result['results'] as $packageResult) { 38 | $packageResult = (array) $packageResult; 39 | $packages = $this->packageDao->getPackagesByName($packageResult['name']); 40 | foreach ($packages as $package) { 41 | $package = (array) $package; 42 | $package['downloads'] = $packageResult['downloads']; 43 | $package['favers'] = $packageResult['favers']; 44 | $this->packageDao->save($package); 45 | } 46 | } 47 | 48 | sleep(self::WAIT_TIME_BETWEEN_REQUESTS); 49 | 50 | ++$i; 51 | } while (isset($result['next'])); 52 | } 53 | 54 | /** 55 | * Performs a request to the API, returns. 56 | * 57 | * @param number $page 58 | */ 59 | private function request($page = 1) 60 | { 61 | $client = new GuzzleHttp\Client(); 62 | $response = $client->get('https://packagist.org/search.json?q=&page='.$page); 63 | 64 | return GuzzleHttp\json_decode($response->getBody(), true); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/templates/Mouf/Packanalyst/Widgets/Node__revert.twig: -------------------------------------------------------------------------------- 1 | {% if children %} 2 | {% for child in this.getChildrenSortedByScore() %} 3 | {{ child.getHtmlRevert() | raw }} 4 | {% endfor %} 5 | {% endif %} 6 | 7 | {% if this.getRevertDepth() != 0 %} 8 | {# FIXME: getDepth doit etre fait par rapport au max de l'arbre global, pas de l'arbre courant! #} 9 | {% for i in 1..this.getRevertDepth() %} 10 |
    11 | {% endfor %} 12 | {% endif %} 13 | 14 | {% if not replacementNode %} 15 |
    16 |
    {{ name }} 17 | {% spaceless %} 18 |
    19 | {% if this.getNbStars() %} 20 | {% for i in 1..this.getNbStars() %} 21 | 22 | {% endfor %} 23 | {% endif %} 24 |
    25 | {% endspaceless %} 26 |
    27 | {% for packageName, versions in this.getImportantPackages() %} 28 |
    29 | {{ packageName }} 30 | 31 | {% for version in versions%} 32 | {{ version }} 33 | {% endfor %} 34 | 35 |
    36 | {% endfor %} 37 | 38 | {% if this.getNotImportantPackages() %} 39 |
    40 | View more packages 41 |
    42 | {% for packageName, versions in this.getNotImportantPackages() %} 43 |
    44 | {{ packageName }} 45 | 46 | {% for version in versions%} 47 | {{ version }} 48 | {% endfor %} 49 | 50 |
    51 | {% endfor %} 52 |
    53 |
    54 | {% endif %} 55 | 56 | 57 |
    58 | {% else %} 59 | {{ toHtml(replacementNode) }} 60 | {% endif %} 61 | 62 | {% if this.getRevertDepth() != 0 %} 63 | {% for i in 1..this.getRevertDepth() %} 64 |
    65 | {% endfor %} 66 | {% endif %} -------------------------------------------------------------------------------- /src/Mouf/Packanalyst/Widgets/Graph.php: -------------------------------------------------------------------------------- 1 | 19 | */ 20 | private $nodesList; 21 | 22 | public function __construct($rootNodeItems, $allItems) 23 | { 24 | foreach ($rootNodeItems as $rootNodeItem) { 25 | $this->rootNode = $this->registerNode($rootNodeItem); 26 | } 27 | 28 | // First pass, let's register all items. 29 | foreach ($allItems as $item) { 30 | $this->registerNode($item); 31 | } 32 | 33 | // Second path, let's register relationships. 34 | foreach ($allItems as $item) { 35 | foreach ($item['inherits'] as $parentItemName) { 36 | if (isset($this->nodesList[$parentItemName])) { 37 | $this->nodesList[$parentItemName]->addChild($this->nodesList[$item['name']]); 38 | } 39 | } 40 | } 41 | 42 | $this->rootNode->setHighlight(true); 43 | } 44 | 45 | private function registerNode($item) 46 | { 47 | $className = $item['name']; 48 | if (!isset($this->nodesList[$className])) { 49 | $this->nodesList[$className] = new Node($className, isset($item['type']) ? $item['type'] : null); 50 | } 51 | if (isset($item['packageName'])) { 52 | $this->nodesList[$className]->registerPackage($item['packageName'], $item['packageVersion'], isset($item['package']['downloads']) ? $item['package']['downloads'] : null, isset($item['package']['favers']) ? $item['package']['favers'] : null); 53 | } 54 | 55 | return $this->nodesList[$className]; 56 | } 57 | 58 | public function toHtml() 59 | { 60 | echo "
      "; 61 | $this->rootNode->toHtml(); 62 | echo '
    '; 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: php 2 | 3 | jobs: 4 | include: 5 | - stage: test 6 | php: 7.2 7 | before_script: 8 | - echo "extension = mongodb.so" >> ~/.phpenv/versions/$(phpenv version-name)/etc/php.ini 9 | - composer install --prefer-dist --no-interaction 10 | script: 11 | - composer phpstan 12 | - stage: build 13 | services: 14 | - docker 15 | script: 16 | - docker build . --tag thecodingmachine/packanalyst:latest 17 | - | 18 | if [[ "$TRAVIS_PULL_REQUEST" = false && "$TRAVIS_BRANCH" = "master" ]] ; then 19 | docker login -u $DOCKER_USER -p $DOCKER_PASS 20 | docker push thecodingmachine/packanalyst:latest 21 | fi 22 | - stage: deploy 23 | services: 24 | - docker 25 | branches: 26 | only: 27 | - master 28 | cache: 29 | directories: 30 | # We cache the SDK so we don't have to download it again on subsequent builds. 31 | - $HOME/google-cloud-sdk 32 | env: 33 | - CLOUDSDK_CORE_DISABLE_PROMPTS=1 34 | script: 35 | - | 36 | if [[ "$TRAVIS_PULL_REQUEST" = false ]] ; then 37 | if [ ! -d $HOME/google-cloud-sdk/bin ]; then 38 | # The install script errors if this directory already exists, 39 | # but Travis already creates it when we mark it as cached. 40 | rm -rf $HOME/google-cloud-sdk; 41 | # The install script is overly verbose, which sometimes causes 42 | # problems on Travis, so ignore stdout. 43 | curl https://sdk.cloud.google.com | bash > /dev/null; 44 | fi 45 | source $HOME/google-cloud-sdk/path.bash.inc 46 | gcloud components update kubectl 47 | gcloud version 48 | 49 | echo "$GCLOUD_SERVICE_KEY" > key.json 50 | gcloud auth activate-service-account --key-file key.json 51 | gcloud config set project $GCLOUD_PROJECT 52 | gcloud container clusters get-credentials $GKE_CLUSTER --zone $ZONE --project $GCLOUD_PROJECT 53 | kubectl create namespace packanalyst || true 54 | kubectl -n packanalyst delete deployment --all 55 | kubectl -n packanalyst delete secret tcmregistry || true 56 | cd kubernetes 57 | kubectl -n packanalyst apply -f . 58 | fi 59 | -------------------------------------------------------------------------------- /src/Mouf/Packanalyst/Services/ComposerSrcDirectoryFinder.php: -------------------------------------------------------------------------------- 1 | 9 | */ 10 | class ComposerSrcDirectoryFinder 11 | { 12 | /** 13 | * @param string $composerJsonPath Path to the composer file 14 | * @param bool $useAutoloadDev Include files for autoload dev? 15 | * 16 | * @return array The directories containing PHP code. 17 | */ 18 | public static function getComposerSrcDirs($composerJsonPath, $useAutoloadDev = false) 19 | { 20 | $composer = json_decode(file_get_contents($composerJsonPath), true); 21 | 22 | if (!$composer) { 23 | return []; 24 | } 25 | 26 | $srcDirs = []; 27 | 28 | $autoloadTags = ['autoload']; 29 | if ($useAutoloadDev) { 30 | $autoloadTags[] = ['autoload-dev']; 31 | } 32 | 33 | foreach ($autoloadTags as $autoload) { 34 | foreach (['psr-0', 'psr-4'] as $psr) { 35 | if (isset($composer[$autoload][$psr])) { 36 | $map = $composer[$autoload][$psr]; 37 | foreach ($map as $namespace => $paths) { 38 | if (!is_array($paths)) { 39 | $paths = [$paths]; 40 | } 41 | 42 | $paths = array_map(function ($path) { 43 | return trim($path, '\\/'); 44 | }, $paths); 45 | 46 | $srcDirs = array_merge($srcDirs, $paths); 47 | } 48 | } 49 | } 50 | 51 | if (isset($composer[$autoload]['classmap'])) { 52 | foreach ($composer[$autoload]['classmap'] as $classMapDir) { 53 | if (strrpos($classMapDir, '.php') !== strlen($classMapDir) - 4) { 54 | $srcDirs[] = $classMapDir; 55 | } 56 | } 57 | } 58 | } 59 | 60 | // Remove duplicates: 61 | $srcDirs = array_flip(array_flip($srcDirs)); 62 | 63 | $srcDirs = array_filter($srcDirs, function ($path) { 64 | return strpos($path, '..') === false; 65 | }); 66 | 67 | return $srcDirs; 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/templates/Mouf/Html/Template/BootstrapTemplate.twig: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | {% if favIconUrl %} 7 | 8 | {% endif %} 9 | {{ this.title }} 10 | {% if this.webLibraryManager %} 11 | {{ toHtml(this.webLibraryManager) }} 12 | {% endif %} 13 | 18 | 19 | {% set contentSize = 12 %} 20 | {% if this.left %} 21 | {% set leftHtml %}{{ toHtml(this.left) }}{% endset %} 22 | {% if leftHtml %} 23 | {% set contentSize = contentSize - this.leftColumnSize %} 24 | {% endif %} 25 | {% endif %} 26 | 27 | {% if this.right %} 28 | {% set rightHtml %}{{ toHtml(this.right) }}{% endset %} 29 | {% if rightHtml %} 30 | {% set contentSize = contentSize - this.rightColumnSize %} 31 | {% endif %} 32 | {% endif %} 33 | 34 | {% if this.footer %} 35 | {% set footerHtml %}{{ toHtml(this.footer) }}{% endset %} 36 | {% endif %} 37 | 38 | {% if this.logoUrl %} 39 | 46 | {% endif %} 47 | {{ toHtml(this.header) }} 48 | 49 |
    50 |
    51 | {% if leftHtml %} 52 | 61 | {% endif %} 62 |
    63 | {{ toHtml(this.content) }} 64 |
    65 | {% if rightHtml %} 66 | 75 | {% endif %} 76 |
    77 |
    78 | {{ footerHtml }} 79 | 80 | -------------------------------------------------------------------------------- /src/views/src/less/global.less: -------------------------------------------------------------------------------- 1 | //--------------------------- 2 | // Generic styles 3 | 4 | html, body { 5 | min-height: 100%; 6 | position: relative; 7 | } 8 | body { 9 | position: relative; 10 | font-family: 'Roboto', sans-serif; 11 | font-size: 16px; 12 | font-weight: 300; 13 | } 14 | 15 | strong, b, dt { 16 | font-weight: 500; 17 | } 18 | 19 | table.table { 20 | border: 1px solid #ddd; 21 | margin-top: 10px; 22 | } 23 | 24 | a { 25 | color: #386E9D; 26 | } 27 | 28 | th { 29 | font-weight: @mediumWeight; 30 | } 31 | 32 | p.sub-title { 33 | font-size: 20px; 34 | } 35 | 36 | .well { 37 | border-radius: 0; 38 | background: none; 39 | p { 40 | margin: 0; 41 | } 42 | } 43 | 44 | .navbar-default { 45 | background: @whiteColor url('@{imagePath}bg-head.jpg') repeat-x top right; 46 | border: none; 47 | } 48 | 49 | .navbar-right { 50 | margin-right: 0px; 51 | } 52 | 53 | .homeContainer { 54 | 55 | } 56 | 57 | h2, h3, h4, h5 { 58 | font-weight: @lightWeight; 59 | } 60 | 61 | h1 { 62 | margin-top: 100px; 63 | display: block; 64 | font-size: 35px; 65 | margin-bottom: 20px; 66 | padding-left: 15px; 67 | font-weight: @lightWeight; 68 | &.logo { 69 | margin-top: 65px; 70 | font-weight: @regularWeight; 71 | } 72 | &.small-logo { 73 | margin: 10px 0 10px 0; 74 | font-size: 25px; 75 | float: left; 76 | width: 210x; 77 | font-weight: @regularWeight; 78 | img { 79 | width: 30px; 80 | } 81 | small { 82 | font-size: 12px; 83 | } 84 | } 85 | span { 86 | &.logo { 87 | font-weight: 500; 88 | color: #333333; 89 | } 90 | &.blue { 91 | font-weight: 300; 92 | color: #2F699A; 93 | } 94 | } 95 | small { 96 | display: block; 97 | font-size: 16px; 98 | font-weight: 300; 99 | } 100 | img { 101 | margin-right: 10px; 102 | } 103 | a { 104 | &:hover { 105 | text-decoration: none; 106 | } 107 | } 108 | } 109 | 110 | .search-header { 111 | padding: 10px; 112 | .search-field { 113 | width: 100%; 114 | } 115 | .twitter-typeahead { 116 | float: left; 117 | display: block !important; 118 | margin-right: 20px; 119 | } 120 | } 121 | 122 | #search { 123 | background: @whiteColor url('@{imagePath}bg-head.jpg') repeat-x top right; 124 | margin-bottom: 20px; 125 | } 126 | 127 | //--------------------------- 128 | // Useful classes 129 | .light { 130 | font-weight: 300; 131 | } 132 | 133 | .medium { 134 | font-weight: 500; 135 | } -------------------------------------------------------------------------------- /kubernetes/web.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: "extensions/v1beta1" 3 | kind: "Deployment" 4 | metadata: 5 | name: "web" 6 | labels: 7 | app: "web" 8 | spec: 9 | replicas: 1 10 | strategy: 11 | type: Recreate 12 | rollingUpdate: ~ 13 | selector: 14 | matchLabels: 15 | app: "web" 16 | template: 17 | metadata: 18 | labels: 19 | app: "web" 20 | spec: 21 | # Removing fsGroup because it causes a timeout on mount (changing the group of all files takes too much time) 22 | #securityContext: 23 | # fsGroup: 1000 24 | containers: 25 | - name: "web" 26 | image: "thecodingmachine/packanalyst:latest" 27 | imagePullPolicy: Always 28 | env: 29 | - name: STARTUP_COMMAND_FS 30 | value: "sudo chown docker:docker /var/downloads" 31 | envFrom: 32 | - configMapRef: 33 | name: config 34 | optional: false 35 | volumeMounts: 36 | - name: files-data 37 | mountPath: /var/downloads 38 | resources: 39 | requests: 40 | memory: "1G" 41 | cpu: "1" 42 | limits: 43 | memory: "8G" 44 | cpu: "4" 45 | volumes: 46 | - name: files-data 47 | persistentVolumeClaim: 48 | claimName: files-claim 49 | --- 50 | apiVersion: v1 51 | kind: ConfigMap 52 | data: 53 | # The host name for the Elastic Search server 54 | ELASTICSEARCH_HOST: "elasticsearch" 55 | # The default port to connect to Elastic Search server 56 | ELASTICSEARCH_PORT: "9200" 57 | # A random string. It should be different for any application deployed. 58 | SECRET: "HLxRssObAZpJdFYfHJpT" 59 | # The download directory 60 | DOWNLOAD_DIR: "/var/downloads" 61 | # Connection string to MongoDB 62 | MONGODB_CONNECTIONSTRING: "mongodb://mongo:27017" 63 | # Your Google Analytics key. Leave empty if you want to disable Google Analytics tracking. Don't have a key for your website? Get one here: http://www.google.com/analytics/ 64 | GOOGLE_ANALYTICS_KEY: "UA-25804465-2" 65 | # The base domain name to track (if you are tracking sub-domains). In the form: '.example.com'. Keep this empty if you don't track subdomains. 66 | GOOGLE_ANALYTICS_DOMAIN_NAME: "" 67 | # Set to true to enable debug/development mode. 68 | DEBUG: "true" 69 | # Disable Mouf UI 70 | MOUF_UI: "0" 71 | metadata: 72 | name: config 73 | --- 74 | apiVersion: v1 75 | kind: Service 76 | metadata: 77 | name: web 78 | spec: 79 | selector: 80 | app: "web" 81 | ports: 82 | - name: http 83 | port: 80 84 | targetPort: 80 85 | --- 86 | apiVersion: v1 87 | kind: PersistentVolumeClaim 88 | metadata: 89 | name: files-claim 90 | spec: 91 | accessModes: 92 | - ReadWriteOnce 93 | - ReadOnlyMany 94 | resources: 95 | requests: 96 | storage: 150Gi 97 | --- 98 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Packanalyst 2 | =========== 3 | 4 | A PHP package analyzer for Composer/Packagist. 5 | This is the code of the http://packanalyst.com website. 6 | 7 | Requirements 8 | ------------ 9 | 10 | Packanalyst requires a MongoDB database and an ElasticSearch database. 11 | 12 | Install 13 | ------- 14 | 15 | - Clone the application from the Git repository 16 | - Run `php composer.phar install` 17 | 18 | Configuring Packanalyst 19 | ----------------------- 20 | 21 | Packanalyst is an application based on [Mouf 2](http://mouf-php.com). After installing, you can 22 | configure the application by opening http://[yourserver]/[app_path]/vendor/mouf/mouf. 23 | 24 | - Create a user / password to access the Mouf UI. 25 | - In the Mouf UI, click on "Project > Edit configuration" 26 | - Edit each parameter (usually, the default parameter will be OK). 27 | 28 | In case of troubles, refer to the [Mouf installation guide](http://mouf-php.com/packages/mouf/mouf/doc/installing_mouf.md) 29 | 30 | Initializing the database 31 | ------------------------- 32 | 33 | Once Packanalyst is configured, you must set up the databases (MongoDB and ElasticSearch indexes are configured at this step). 34 | 35 | - Init the databases: `./console.php reset` 36 | 37 | Loading the database with data 38 | ------------------------------ 39 | 40 | The `./console.php` is a CLI based interface to Packanalyst that lets you load some or all packages from Packanalyst. 41 | 42 | Here is a list of some common commands: 43 | 44 | - `./console.php run`: Runs the fetching of ALL packages from Packanalyst. This is a VERY long process (it will take 45 | about a month), and therefore, is only meant to be fully run on Packanalyst production server. You can still use 46 | this command on your local development environment to fetch a few packages to perform tests. 47 | The run command accepts parameters: 48 | - `./console.php run --package="mouf/mouf"` will load only *mouf/mouf* package (useful for testing) 49 | - `./console.php run --retry` will force retrying packages that were considered in error 50 | - `./console.php run --force` will force reloading a package, even if it has not been modified since last check 51 | - `./console.php reset`: deletes all data and restores indexes 52 | - `./console.php get-scores`: retrieves the number of downloads from each package from Packagist 53 | - `./console.php force-refresh` will mark each package for "force retrying" on the next "run" 54 | 55 | MongoDB implementation details 56 | ------------------------------ 57 | 58 | MongoDB item collection: 59 | 60 | ```js 61 | { 62 | "name": "FQDN", 63 | "inherits": [ FQDN1, FQDN2... ], 64 | "globalInherits": [ FQDN1, FQDN2... ], // inherits + inherits of parents, recursively 65 | "type": "class", 66 | "packageName": "packagename", 67 | "packageVersion": "version", 68 | "phpDoc": "doc class", 69 | "refresh": bool // Set to true to force refresh 70 | } 71 | ``` 72 | 73 | ``` 74 | index on: packageName + packageVersion 75 | index on: name 76 | index on: inherits 77 | index on: globalInherits 78 | ``` 79 | 80 | MongoDB package collection: 81 | 82 | ```js 83 | { 84 | packageName: "" 85 | version: "" 86 | type: "" 87 | releaseDate: date 88 | downloads: int 89 | favers: int 90 | } 91 | ``` 92 | 93 | Packanalyst uses Grunt 94 | ------------------------- 95 | Here is the documentation : [Grunt documentation](http://gruntjs.com/) 96 | 97 | Quick use : 98 | 99 | 1. First install NodeJS and add it to your PATH 100 | 2. Go to `src/views`, here are your `Gruntfile.js` & `package.json`. Download your dependencies by using command : `npm install` 101 | 3. Now you can use grunt by using `grunt` or `grunt dev` 102 | -------------------------------------------------------------------------------- /src/Mouf/Packanalyst/Controllers/PackageAnalyzerController.php: -------------------------------------------------------------------------------- 1 | logger = $logger; 61 | $this->template = $template; 62 | $this->content = $content; 63 | $this->twig = $twig; 64 | $this->itemDao = $itemDao; 65 | $this->packageDao = $packageDao; 66 | } 67 | 68 | /** 69 | * @URL package 70 | * @Get 71 | * 72 | * @param string $name 73 | * @param string $version 74 | */ 75 | public function index($name, $version = null) 76 | { 77 | if ($version != null) { 78 | $package = $this->packageDao->get($name, $version); 79 | } else { 80 | $package = $this->packageDao->getLatestPackage($name); 81 | if (isset($package['packageVersion'])) { 82 | $version = $package['packageVersion']; 83 | } 84 | } 85 | 86 | if (!$package) { 87 | header('HTTP/1.0 404 Not Found'); 88 | $this->content->addHtmlElement(new TwigTemplate($this->twig, 'src/views/packageAnalyzer/404.twig', array('packageName' => $name, 'packageVersion' => $version))); 89 | $this->template->toHtml(); 90 | 91 | return; 92 | } 93 | 94 | $allPackages = $this->packageDao->getPackagesByName($name); 95 | $otherVersions = []; 96 | foreach ($allPackages as $package2) { 97 | if ($package2->packageVersion != $version) { 98 | $otherVersions[] = $package2->packageVersion; 99 | } 100 | } 101 | 102 | // Let's sort alphabetically. 103 | $itemsList = $this->itemDao->findItemsByPackageVersion($name, $version, ['sort'=> ['name' => 1]]); 104 | 105 | 106 | $this->template->setTitle('Packanalyst | Package '.$name.' ('.$version.')'); 107 | 108 | \Mouf::getSearchBlock()->setSearch($name); 109 | 110 | // Let's add the twig file to the template. 111 | $this->content->addHtmlElement(new TwigTemplate($this->twig, 'src/views/packageAnalyzer/index.twig', array('package' => $package, 'itemsList' => $itemsList, 'otherVersions' => $otherVersions))); 112 | $this->template->toHtml(); 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /src/Mouf/Packanalyst/ClassesDetector.php: -------------------------------------------------------------------------------- 1 | 23 | */ 24 | class ClassesDetector extends NodeVisitorAbstract 25 | { 26 | private $parser; 27 | private $logger; 28 | private $itemDao; 29 | 30 | public function __construct(LoggerInterface $logger, ItemDao $itemDao) 31 | { 32 | $this->parser = (new ParserFactory())->create(ParserFactory::PREFER_PHP7); 33 | $this->logger = $logger; 34 | $this->itemDao = $itemDao; 35 | } 36 | 37 | /** 38 | * Returns the classes / interfaces / traits / functions of the package. 39 | * 40 | * @return array 41 | */ 42 | public function storePackage($basePath, array $packageVersion) 43 | { 44 | $traverser = new NodeTraverser(); 45 | 46 | $storeInDbNodeVisitor = new StoreInDbNodeVisitor($packageVersion, $this->itemDao); 47 | 48 | $traverser->addVisitor(new NameResolver()); // we will need resolved names 49 | $traverser->addVisitor($storeInDbNodeVisitor); // our own node visitor 50 | 51 | 52 | if (file_exists($basePath.'/composer.json')) { 53 | $srcDirs = ComposerSrcDirectoryFinder::getComposerSrcDirs($basePath.'/composer.json'); 54 | 55 | $files = []; 56 | foreach ($srcDirs as $dir) { 57 | $dirFiles = new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($basePath.'/'.$dir)); 58 | $dirFiles = new \RegexIterator($dirFiles, '/\.php$/'); 59 | $dirFiles = new \CallbackFilterIterator($dirFiles, function ($file) { 60 | return (strpos($file, 'vendor/') === false) && (strpos($file, 'fixtures/') === false); 61 | }); 62 | foreach ($dirFiles as $file) { 63 | $files[] = (string) $file; 64 | } 65 | } 66 | // Last deduplicate: 67 | $files = array_flip(array_flip($files)); 68 | } else { 69 | // Composer.json not found... this is a weird case... let's go back to full directory scanning. 70 | $files = new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($basePath)); 71 | $files = new \RegexIterator($files, '/\.php$/'); 72 | $files = new \CallbackFilterIterator($files, function ($file) { 73 | return (strpos($file, 'vendor/') === false) && (strpos($file, 'fixtures/') === false); 74 | }); 75 | } 76 | 77 | foreach ($files as $file) { 78 | try { 79 | $relativeFileName = substr($file, strlen($basePath)); 80 | $storeInDbNodeVisitor->setFileName($relativeFileName); 81 | 82 | // read the file that should be converted 83 | $code = file_get_contents($file); 84 | 85 | // parse 86 | 87 | $stmts = $this->parser->parse($code); 88 | 89 | // traverse 90 | $stmts = $traverser->traverse($stmts); 91 | } catch (Error $e) { 92 | $this->logger->warning('PHP error detected in file {file}. Ignoring file. Error: {errorMsg}', [ 93 | 'file' => $file, 94 | 'errorMsg' => $e->getMessage(), 95 | 'exception' => $e, 96 | ] 97 | ); 98 | } 99 | } 100 | 101 | //return $classMap; 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /src/Mouf/Packanalyst/Controllers/RootController.php: -------------------------------------------------------------------------------- 1 | template = $template; 42 | $this->content = $content; 43 | $this->elasticSearchService = $elasticSearchService; 44 | $this->packageDao = $packageDao; 45 | $this->itemDao = $itemDao; 46 | $this->twig = $twig; 47 | } 48 | 49 | /** 50 | * Page displayed when a user arrives on your web application. 51 | * 52 | * @URL / 53 | */ 54 | public function index() 55 | { 56 | $this->template->setTitle('Packanalyst | Explore PHP classes from Packagist'); 57 | //$this->template->setContainerClass('homeContainer container'); 58 | $this->template->setContainerClass('homeContainer'); 59 | array_shift(\Mouf::getBootstrapNavBar()->children); 60 | array_shift(\Mouf::getBootstrapNavBar()->children); 61 | $this->content->addFile(ROOT_PATH.'src/views/root/index.php', $this); 62 | $this->template->toHtml(); 63 | } 64 | 65 | /** 66 | * @URL /suggest 67 | */ 68 | public function suggest($q) 69 | { 70 | header('Content-type: application/json'); 71 | 72 | $results = $this->elasticSearchService->suggestItemName2($q); 73 | 74 | $jsonArr = array_map(function ($item) { 75 | return [ 76 | 'value' => $item['_source']['name'], 77 | ]; 78 | }, $results['hits']); 79 | 80 | echo json_encode($jsonArr); 81 | } 82 | 83 | /** 84 | * @URL /search 85 | * 86 | * @param string $q The query string 87 | */ 88 | public function search($q, $page = 0) 89 | { 90 | // If query is a valid item of package, let's go to the dedicated page. 91 | $item = $this->itemDao->getItemsByName($q); 92 | if (count($item->toArray()) != 0) { 93 | header('Location: '.ROOT_URL.'class?q='.urlencode($q)); 94 | 95 | return; 96 | } 97 | 98 | $package = $this->packageDao->getLatestPackage($q); 99 | if ($package != null) { 100 | // Let's grab the first 101 | header('Location: '.ROOT_URL.'package?name='.urlencode($q).'&version='.$package['packageVersion']); 102 | 103 | return; 104 | } 105 | 106 | $searchResults = $this->elasticSearchService->suggestItemName2($q, 50, $page * 50); 107 | $totalCount = $searchResults['total']; 108 | $hits = $searchResults['hits']; 109 | $nbPages = floor($totalCount / 50); 110 | 111 | $this->template->setTitle('Packanalyst | Search results for '.$q); 112 | 113 | \Mouf::getSearchBlock()->setSearch($q); 114 | 115 | $this->content->addHtmlElement(new TwigTemplate($this->twig, 'src/views/root/search.twig', 116 | array( 117 | 'searchResults' => $hits, 118 | 'totalCount' => $totalCount, 119 | 'query' => $q, 120 | 'nbPages' => $nbPages, 121 | 'page' => $page, 122 | ))); 123 | $this->template->toHtml(); 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /src/Mouf/Packanalyst/Command/RunCommand.php: -------------------------------------------------------------------------------- 1 | setName('run') 27 | ->setDescription('Runs Packanalyst update.') 28 | ->setDefinition(array( 29 | new InputOption('package', null, InputOption::VALUE_REQUIRED, 'Name of the package to analyze'), 30 | new InputOption('retry', null, InputOption::VALUE_NONE, 'Retry packages previously in error'), 31 | new InputOption('force', null, InputOption::VALUE_NONE, 'Forces packages to update even if the package has not been updated'), 32 | )) 33 | /*->setDefinition(array( 34 | new InputOption('name', null, InputOption::VALUE_REQUIRED, 'Name of the package'), 35 | new InputOption('description', null, InputOption::VALUE_REQUIRED, 'Description of package'), 36 | new InputOption('author', null, InputOption::VALUE_REQUIRED, 'Author name of package'), 37 | // new InputOption('version', null, InputOption::VALUE_NONE, 'Version of package'), 38 | new InputOption('homepage', null, InputOption::VALUE_REQUIRED, 'Homepage of package'), 39 | new InputOption('require', null, InputOption::VALUE_IS_ARRAY | InputOption::VALUE_REQUIRED, 'Package to require with a version constraint, e.g. foo/bar:1.0.0 or foo/bar=1.0.0 or "foo/bar 1.0.0"'), 40 | new InputOption('require-dev', null, InputOption::VALUE_IS_ARRAY | InputOption::VALUE_REQUIRED, 'Package to require for development with a version constraint, e.g. foo/bar:1.0.0 or foo/bar=1.0.0 or "foo/bar 1.0.0"'), 41 | new InputOption('stability', 's', InputOption::VALUE_REQUIRED, 'Minimum stability (empty or one of: '.implode(', ', array_keys(BasePackage::$stabilities)).')'), 42 | new InputOption('license', 'l', InputOption::VALUE_REQUIRED, 'License of package'), 43 | ))*/ 44 | ->setHelp(<<run command loads all new packages from Composer and uploads them in database. 46 | EOT 47 | ) 48 | ; 49 | } 50 | 51 | protected function execute(InputInterface $input, OutputInterface $output) 52 | { 53 | \Mouf::getDownloadLock()->acquireLock(); 54 | 55 | $fetchDataService = \Mouf::getFetchDataService(); 56 | $fetchDataService->setDownloadManager($this->getDownloadManager()); 57 | $fetchDataService->setPackagistRepository($this->getPackagistRepository()); 58 | 59 | $package = $input->getOption('package'); 60 | $retry = $input->getOption('retry'); 61 | $force = $input->getOption('force'); 62 | 63 | if ($package) { 64 | $fetchDataService->setForcedPackage($package); 65 | } 66 | if ($retry) { 67 | $fetchDataService->setRetryOnError(true); 68 | } 69 | if ($force) { 70 | $fetchDataService->setForce(true); 71 | } 72 | 73 | $fetchDataService->run(); 74 | } 75 | 76 | /** 77 | * @return ComposerRepository 78 | */ 79 | private function getPackagistRepository() 80 | { 81 | if (!$this->repos) { 82 | $this->repos = Factory::createDefaultRepositories($this->getIO()); 83 | } 84 | 85 | return $this->repos['packagist']; 86 | } 87 | 88 | private function getDownloadManager() 89 | { 90 | if (!$this->downloadManager) { 91 | $config = Factory::createConfig(); 92 | $config->merge(array('config' => array( 93 | 'preferred-install' => 'dist', 94 | ))); 95 | $factory = new Factory(); 96 | 97 | $this->downloadManager = $factory->createDownloadManager($this->getIO(), $config); 98 | } 99 | 100 | return $this->downloadManager; 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /mouf/installs_app.php: -------------------------------------------------------------------------------- 1 | 9 | array ( 10 | 'status' => 'done', 11 | 'type' => 'class', 12 | 'class' => 'Mouf\\Utils\\Constants\\DebugInstaller', 13 | 'package' => 'mouf/utils.constants.debug', 14 | ), 15 | 1 => 16 | array ( 17 | 'status' => 'done', 18 | 'type' => 'class', 19 | 'class' => 'Mouf\\Utils\\Constants\\SecretInstaller', 20 | 'package' => 'mouf/utils.constants.secret', 21 | ), 22 | 2 => 23 | array ( 24 | 'status' => 'done', 25 | 'type' => 'class', 26 | 'class' => 'Mouf\\Utils\\Common\\Doctrine\\Cache\\CacheInstaller', 27 | 'package' => 'mouf/utils.common.doctrine-cache-wrapper', 28 | ), 29 | 3 => 30 | array ( 31 | 'status' => 'done', 32 | 'type' => 'class', 33 | 'class' => 'Mouf\\Utils\\Common\\Doctrine\\Annotations\\AnnotationReaderInstaller', 34 | 'package' => 'mouf/utils.common.doctrine-annotations-wrapper', 35 | ), 36 | 4 => 37 | array ( 38 | 'status' => 'done', 39 | 'type' => 'class', 40 | 'class' => 'Mouf\\Html\\Renderer\\Twig\\MoufTwigEnvironmentInstaller2', 41 | 'package' => 'mouf/html.renderer.twig-extensions', 42 | ), 43 | 5 => 44 | array ( 45 | 'status' => 'done', 46 | 'type' => 'class', 47 | 'class' => 'Mouf\\Utils\\Cache\\FileCacheInstaller', 48 | 'package' => 'mouf/utils.cache.file-cache', 49 | ), 50 | 6 => 51 | array ( 52 | 'status' => 'done', 53 | 'type' => 'class', 54 | 'class' => 'Mouf\\Utils\\Log\\Psr\\ErrorLogLoggerInstaller', 55 | 'package' => 'mouf/utils.log.psr.errorlog_logger', 56 | ), 57 | 7 => 58 | array ( 59 | 'status' => 'done', 60 | 'type' => 'class', 61 | 'class' => 'Mouf\\Utils\\Cache\\ApcCacheInstaller', 62 | 'package' => 'mouf/utils.cache.apc-cache', 63 | ), 64 | 8 => 65 | array ( 66 | 'status' => 'done', 67 | 'type' => 'file', 68 | 'file' => 'src/install.php', 69 | 'package' => 'mouf/html.renderer', 70 | ), 71 | 9 => 72 | array ( 73 | 'status' => 'done', 74 | 'type' => 'class', 75 | 'class' => 'Mouf\\Html\\Utils\\WebLibraryManager\\WebLibraryManagerInstaller', 76 | 'package' => 'mouf/html.utils.weblibrarymanager', 77 | ), 78 | 10 => 79 | array ( 80 | 'status' => 'done', 81 | 'type' => 'file', 82 | 'file' => 'src/install.php', 83 | 'package' => 'mouf/html.widgets.menu', 84 | ), 85 | 11 => 86 | array ( 87 | 'status' => 'done', 88 | 'type' => 'file', 89 | 'file' => 'src/install.php', 90 | 'package' => 'mouf/utils.session.session-manager', 91 | ), 92 | 12 => 93 | array ( 94 | 'status' => 'done', 95 | 'type' => 'file', 96 | 'file' => 'src/install.php', 97 | 'package' => 'mouf/html.widgets.messageservice', 98 | ), 99 | 13 => 100 | array ( 101 | 'status' => 'done', 102 | 'type' => 'class', 103 | 'class' => 'Mouf\\Html\\Template\\BootstrapTemplateInstaller', 104 | 'package' => 'mouf/html.template.bootstrap', 105 | ), 106 | 14 => 107 | array ( 108 | 'status' => 'done', 109 | 'type' => 'file', 110 | 'file' => 'src/install.php', 111 | 'package' => 'mouf/utils.i18n.fine', 112 | ), 113 | 15 => 114 | array ( 115 | 'status' => 'done', 116 | 'type' => 'file', 117 | 'file' => 'src/validatorsInstall.php', 118 | 'package' => 'mouf/utils.common.validators', 119 | ), 120 | 16 => 121 | array ( 122 | 'status' => 'done', 123 | 'type' => 'file', 124 | 'file' => 'src/splashCommonInstall.php', 125 | 'package' => 'mouf/mvc.splash-common', 126 | ), 127 | 17 => 128 | array ( 129 | 'status' => 'done', 130 | 'type' => 'file', 131 | 'file' => 'src/install.php', 132 | 'package' => 'mouf/utils.cache.no-cache', 133 | ), 134 | 18 => 135 | array ( 136 | 'status' => 'done', 137 | 'type' => 'url', 138 | 'url' => 'splashinstall/', 139 | 'package' => 'mouf/mvc.splash', 140 | ), 141 | 19 => 142 | array ( 143 | 'status' => 'done', 144 | 'type' => 'url', 145 | 'url' => 'splashinstall/writeHtAccess', 146 | 'package' => 'mouf/mvc.splash', 147 | ), 148 | 20 => 149 | array ( 150 | 'status' => 'done', 151 | 'type' => 'file', 152 | 'file' => 'install.php', 153 | 'package' => 'mouf/modules.google-analytics', 154 | ), 155 | 21 => 156 | array ( 157 | 'status' => 'done', 158 | 'type' => 'file', 159 | 'file' => 'src/install.php', 160 | 'package' => 'mouf/utils.log.errorlog_logger', 161 | ), 162 | ); -------------------------------------------------------------------------------- /src/Mouf/Packanalyst/Dao/PackageDao.php: -------------------------------------------------------------------------------- 1 | collection = $collection; 24 | $this->elasticSearchService = $elasticSearchService; 25 | $this->itemDao = $itemDao; 26 | } 27 | 28 | public function createIndex() 29 | { 30 | $this->collection->createIndex([ 31 | 'packageName' => 1, 32 | 'packageVersion' => 1, 33 | ]); 34 | } 35 | 36 | /** 37 | * Drops the complete collection. 38 | */ 39 | public function drop() 40 | { 41 | $this->collection->drop(); 42 | } 43 | 44 | /** 45 | * Deletes all items relative to this package version. 46 | * 47 | * @param string $packageName 48 | * @param string $packageVersion 49 | */ 50 | public function deletePackage($packageName, $packageVersion) 51 | { 52 | $this->collection->deleteMany([ 53 | 'packageName' => $packageName, 54 | 'packageVersion' => $packageVersion, 55 | ]); 56 | } 57 | 58 | /** 59 | * Returns a package by name and version. 60 | * 61 | * @param string $packageName 62 | * @param string $packageVersion 63 | * 64 | * @return array|null 65 | */ 66 | public function get($packageName, $packageVersion) 67 | { 68 | return (array) $this->collection->findOne([ 69 | 'packageName' => $packageName, 70 | 'packageVersion' => $packageVersion, 71 | ]); 72 | } 73 | 74 | public function getPackagesByName($packageName) 75 | { 76 | return $this->collection->find([ 77 | 'packageName' => $packageName, 78 | ]); 79 | } 80 | 81 | /** 82 | * @param string $packageName 83 | * 84 | * @return array 85 | */ 86 | public function getLatestPackage($packageName) 87 | { 88 | $packages = $this->getPackagesByName($packageName)->toArray(); 89 | 90 | if (count($packages) == 0) { 91 | return; 92 | } 93 | 94 | // Is there a dev-master? 95 | foreach ($packages as $package) { 96 | if ($package->packageVersion == 'dev-master') { 97 | return (array) $package; 98 | } 99 | } 100 | 101 | $latestVersion = '0.0.0'; 102 | $selectedPackage = reset($packages); 103 | 104 | foreach ($packages as $package) { 105 | $version = ltrim($package->packageVersion, 'v'); 106 | if (version_compare($version, $latestVersion) > 0) { 107 | $latestVersion = $version; 108 | $selectedPackage = $package; 109 | } 110 | } 111 | 112 | return (array) $selectedPackage; 113 | } 114 | 115 | /** 116 | * Creates or update a package in MongoDB from the Package passed in parameter. 117 | * 118 | * @param Package $package 119 | * 120 | * @return array 121 | */ 122 | public function createOrUpdatePackage(Package $package) 123 | { 124 | $packageVersion = $this->get($package->getName(), $package->getPrettyVersion()); 125 | 126 | if (!$packageVersion) { 127 | $packageVersion = [ 128 | 'packageName' => $package->getName(), 129 | 'packageVersion' => $package->getPrettyVersion(), 130 | ]; 131 | } 132 | 133 | if ($package->getReleaseDate() === null) { 134 | throw new \Exception('The package does not have a valid release date.'); 135 | } 136 | $packageVersion['releaseDate'] = new \MongoDB\BSON\UTCDateTime($package->getReleaseDate()->getTimestamp()*1000); 137 | $packageVersion['type'] = $package->getType(); 138 | $packageVersion['sourceUrl'] = $package->getSourceUrl(); 139 | $packageVersion['realVersion'] = $package->getVersion(); 140 | 141 | if ($package instanceof CompletePackage) { 142 | $packageVersion['description'] = $package->getDescription(); 143 | } 144 | 145 | $this->save($packageVersion); 146 | 147 | // Boost = 1 + download/10 + favers 148 | // TODO: we could improve the score of packages by the number of times they are referred by other packages. 149 | $score = 1; 150 | if (isset($packageVersion['downloads'])) { 151 | $score += $packageVersion['downloads'] / 10; 152 | } 153 | if (isset($packageVersion['favers'])) { 154 | $score += $packageVersion['favers']; 155 | } 156 | 157 | $this->itemDao->applyScore($package->getName(), $score); 158 | 159 | $this->elasticSearchService->storeItemName($package->getName(), 'package', $score); 160 | 161 | return $packageVersion; 162 | } 163 | 164 | public function save(array $packageVersion) 165 | { 166 | if (isset($packageVersion['_id'])) { 167 | $this->collection->findOneAndReplace(['_id'=>$packageVersion['_id']], $packageVersion); 168 | } else { 169 | $this->collection->insertOne($packageVersion); 170 | } 171 | } 172 | 173 | public function applyOnAllPackages(callable $callback) 174 | { 175 | foreach ($this->collection->find() as $item) { 176 | $callback((array) $item); 177 | } 178 | } 179 | 180 | /** 181 | * Marks all packages for refresh. 182 | */ 183 | public function refreshAllPackages() 184 | { 185 | $this->collection->updateMany( 186 | array(), 187 | array('$set' => array('refresh' => true)), 188 | array('multiple' => true) 189 | ); 190 | } 191 | } 192 | -------------------------------------------------------------------------------- /src/views/src/less/variables.less: -------------------------------------------------------------------------------- 1 | //--------------------------- 2 | // Color variables 3 | @darkColor: #151515; 4 | @whiteColor: #FFF; 5 | 6 | @mainColor: #326c9e; 7 | 8 | //--------------------------- 9 | // Font variables 10 | @mainFont: 'Roboto', sans-serif; 11 | @secondaryFont: ''; 12 | @lightWeight: 300; 13 | @regularWeight: 400; 14 | @mediumWeight: 500; 15 | @boldWeight: 600; 16 | 17 | //--------------------------- 18 | // Path variables 19 | @imagePath: 'images/'; 20 | 21 | //--------------------------- 22 | // A set of useful LESS mixins 23 | //--------------------------- 24 | 25 | .gradient(@color: #2d6697, @start: #2d6697, @stop: #4181b7) { 26 | background: @color; 27 | background: -webkit-gradient(linear, 28 | left bottom, 29 | left top, 30 | color-stop(0, @start), 31 | color-stop(1, @stop)); 32 | background: -ms-linear-gradient(bottom, 33 | @start, 34 | @stop); 35 | background: -moz-linear-gradient(center bottom, 36 | @start 0%, 37 | @stop 100%); 38 | background: -o-linear-gradient(@stop, 39 | @start); 40 | filter: e(%("progid:DXImageTransform.Microsoft.gradient(startColorstr='%d', endColorstr='%d', GradientType=0)",@stop,@start)); 41 | } 42 | .bw-gradient(@color: #F5F5F5, @start: 0, @stop: 255) { 43 | background: @color; 44 | background: -webkit-gradient(linear, 45 | left bottom, 46 | left top, 47 | color-stop(0, rgb(@start,@start,@start)), 48 | color-stop(1, rgb(@stop,@stop,@stop))); 49 | background: -ms-linear-gradient(bottom, 50 | rgb(@start,@start,@start) 0%, 51 | rgb(@stop,@stop,@stop) 100%); 52 | background: -moz-linear-gradient(center bottom, 53 | rgb(@start,@start,@start) 0%, 54 | rgb(@stop,@stop,@stop) 100%); 55 | background: -o-linear-gradient(rgb(@stop,@stop,@stop), 56 | rgb(@start,@start,@start)); 57 | filter: e(%("progid:DXImageTransform.Microsoft.gradient(startColorstr='%d', endColorstr='%d', GradientType=0)",rgb(@stop,@stop,@stop),rgb(@start,@start,@start))); 58 | } 59 | .bordered(@top-color: #EEE, @right-color: #EEE, @bottom-color: #EEE, @left-color: #EEE) { 60 | border-top: solid 1px @top-color; 61 | border-left: solid 1px @left-color; 62 | border-right: solid 1px @right-color; 63 | border-bottom: solid 1px @bottom-color; 64 | } 65 | .drop-shadow(@x-axis: 0, @y-axis: 1px, @blur: 2px, @alpha: 0.1) { 66 | -webkit-box-shadow: @x-axis @y-axis @blur rgba(0, 0, 0, @alpha); 67 | -moz-box-shadow: @x-axis @y-axis @blur rgba(0, 0, 0, @alpha); 68 | box-shadow: @x-axis @y-axis @blur rgba(0, 0, 0, @alpha); 69 | } 70 | .rounded(@radius: 2px) { 71 | -webkit-border-radius: @radius; 72 | -moz-border-radius: @radius; 73 | border-radius: @radius; 74 | } 75 | .border-radius(@topright: 0, @bottomright: 0, @bottomleft: 0, @topleft: 0) { 76 | -webkit-border-top-right-radius: @topright; 77 | -webkit-border-bottom-right-radius: @bottomright; 78 | -webkit-border-bottom-left-radius: @bottomleft; 79 | -webkit-border-top-left-radius: @topleft; 80 | -moz-border-radius-topright: @topright; 81 | -moz-border-radius-bottomright: @bottomright; 82 | -moz-border-radius-bottomleft: @bottomleft; 83 | -moz-border-radius-topleft: @topleft; 84 | border-top-right-radius: @topright; 85 | border-bottom-right-radius: @bottomright; 86 | border-bottom-left-radius: @bottomleft; 87 | border-top-left-radius: @topleft; 88 | .background-clip(padding-box); 89 | } 90 | .opacity(@opacity: 0.5) { 91 | -moz-opacity: @opacity; 92 | -khtml-opacity: @opacity; 93 | -webkit-opacity: @opacity; 94 | opacity: @opacity; 95 | @opperc: @opacity * 100; 96 | -ms-filter: ~"progid:DXImageTransform.Microsoft.Alpha(opacity=@{opperc})"; 97 | filter: ~"alpha(opacity=@{opperc})"; 98 | } 99 | .transition-duration(@duration: 0.2s) { 100 | -moz-transition-duration: @duration; 101 | -webkit-transition-duration: @duration; 102 | -o-transition-duration: @duration; 103 | transition-duration: @duration; 104 | } 105 | .transform(...) { 106 | -webkit-transform: @arguments; 107 | -moz-transform: @arguments; 108 | -o-transform: @arguments; 109 | -ms-transform: @arguments; 110 | transform: @arguments; 111 | } 112 | .rotation(@deg:5deg){ 113 | .transform(rotate(@deg)); 114 | } 115 | .scale(@ratio:1.5){ 116 | .transform(scale(@ratio)); 117 | } 118 | .transition(@duration:0.2s, @ease:ease-out) { 119 | -webkit-transition: all @duration @ease; 120 | -moz-transition: all @duration @ease; 121 | -o-transition: all @duration @ease; 122 | transition: all @duration @ease; 123 | } 124 | .inner-shadow(@horizontal:0, @vertical:1px, @blur:2px, @alpha: 0.4) { 125 | -webkit-box-shadow: inset @horizontal @vertical @blur rgba(0, 0, 0, @alpha); 126 | -moz-box-shadow: inset @horizontal @vertical @blur rgba(0, 0, 0, @alpha); 127 | box-shadow: inset @horizontal @vertical @blur rgba(0, 0, 0, @alpha); 128 | } 129 | .box-shadow(@arguments) { 130 | -webkit-box-shadow: @arguments; 131 | -moz-box-shadow: @arguments; 132 | box-shadow: @arguments; 133 | } 134 | .box-sizing(@sizing: border-box) { 135 | -ms-box-sizing: @sizing; 136 | -moz-box-sizing: @sizing; 137 | -webkit-box-sizing: @sizing; 138 | box-sizing: @sizing; 139 | } 140 | .user-select(@argument: none) { 141 | -webkit-user-select: @argument; 142 | -moz-user-select: @argument; 143 | -ms-user-select: @argument; 144 | user-select: @argument; 145 | } 146 | .columns(@colwidth: 250px, @colcount: 0, @colgap: 50px, @columnRuleColor: #EEE, @columnRuleStyle: solid, @columnRuleWidth: 1px) { 147 | -moz-column-width: @colwidth; 148 | -moz-column-count: @colcount; 149 | -moz-column-gap: @colgap; 150 | -moz-column-rule-color: @columnRuleColor; 151 | -moz-column-rule-style: @columnRuleStyle; 152 | -moz-column-rule-width: @columnRuleWidth; 153 | -webkit-column-width: @colwidth; 154 | -webkit-column-count: @colcount; 155 | -webkit-column-gap: @colgap; 156 | -webkit-column-rule-color: @columnRuleColor; 157 | -webkit-column-rule-style: @columnRuleStyle; 158 | -webkit-column-rule-width: @columnRuleWidth; 159 | column-width: @colwidth; 160 | column-count: @colcount; 161 | column-gap: @colgap; 162 | column-rule-color: @columnRuleColor; 163 | column-rule-style: @columnRuleStyle; 164 | column-rule-width: @columnRuleWidth; 165 | } 166 | .translate(@x:0, @y:0) { 167 | .transform(translate(@x, @y)); 168 | } 169 | .background-clip(@argument: padding-box) { 170 | -moz-background-clip: @argument; 171 | -webkit-background-clip: @argument; 172 | background-clip: @argument; 173 | } -------------------------------------------------------------------------------- /src/Mouf/Packanalyst/Services/StoreInDbNodeVisitor.php: -------------------------------------------------------------------------------- 1 | 16 | */ 17 | class StoreInDbNodeVisitor extends NodeVisitorAbstract 18 | { 19 | private $package; 20 | private $itemDao; 21 | private $fileName; 22 | 23 | /** 24 | * Classes/interfaces/traits being used in currently parsed class/interface/trait/function. 25 | * The key is the class name. 26 | * 27 | * @var array 28 | */ 29 | private $uses; 30 | 31 | public function __construct($package, ItemDao $itemDao) 32 | { 33 | $this->package = $package; 34 | $this->itemDao = $itemDao; 35 | } 36 | 37 | public function setFileName($fileName) 38 | { 39 | $this->fileName = $fileName; 40 | } 41 | 42 | public function enterNode(Node $node) 43 | { 44 | // Each time we enter in a class or interface or trait or function, we reset the "uses" array. 45 | if ($node instanceof Stmt\Class_ || $node instanceof Stmt\Interface_ 46 | || $node instanceof Stmt\Trait_ || $node instanceof Stmt\Function_) { 47 | $this->uses = []; 48 | } 49 | } 50 | 51 | public function leaveNode(Node $node) 52 | { 53 | /*if ($node instanceof Node\Name) { 54 | return new Node\Name($node->toString('_')); 55 | }*/ 56 | if ($node instanceof Stmt\Class_ || $node instanceof Stmt\Interface_ 57 | || $node instanceof Stmt\Trait_ || $node instanceof Stmt\Function_) { 58 | $item = []; 59 | 60 | if ($node->name === null) { 61 | // Anonymous class 62 | return; 63 | } 64 | $itemName = $node->namespacedName->toString(); 65 | 66 | $item['name'] = $this->ensureUtf8($itemName); 67 | $comment = $node->getDocComment(); 68 | if ($comment) { 69 | $item['phpDoc'] = $this->ensureUtf8($node->getDocComment()->getText()); 70 | } 71 | 72 | $item['packageName'] = $this->package['packageName']; 73 | $item['packageVersion'] = $this->package['packageVersion']; 74 | $item['fileName'] = $this->fileName; 75 | unset($this->uses[$itemName]); 76 | $item['uses'] = array_keys($this->uses); 77 | 78 | if ($node instanceof Stmt\Class_) { 79 | $item['type'] = ItemDao::TYPE_CLASS; 80 | 81 | $inherits = []; 82 | if ($node->extends) { 83 | $inherits[] = $this->ensureUtf8($node->extends->toString()); 84 | } 85 | 86 | foreach ($node->implements as $implement) { 87 | $inherits[] = $this->ensureUtf8($implement->toString()); 88 | } 89 | $item['inherits'] = $inherits; 90 | } elseif ($node instanceof Stmt\Interface_) { 91 | $item['type'] = ItemDao::TYPE_INTERFACE; 92 | 93 | $inherits = []; 94 | 95 | foreach ($node->extends as $extend) { 96 | $inherits[] = $this->ensureUtf8($extend->toString()); 97 | } 98 | $item['inherits'] = $inherits; 99 | } elseif ($node instanceof Stmt\Trait_) { 100 | $item['type'] = ItemDao::TYPE_TRAIT; 101 | } elseif ($node instanceof Stmt\Function_) { 102 | $item['type'] = ItemDao::TYPE_FUNCTION; 103 | } 104 | 105 | $this->itemDao->save($item); 106 | /*} elseif ($node instanceof Node\Name) { 107 | $this->uses[$node->toString()] = true;*/ 108 | } elseif ($node instanceof Stmt\Const_) { 109 | foreach ($node->consts as $const) { 110 | $this->uses[$const->namespacedName->toString()] = true; 111 | } 112 | } elseif ($node instanceof Expr\StaticCall 113 | || $node instanceof Expr\StaticPropertyFetch 114 | || $node instanceof Expr\ClassConstFetch 115 | || $node instanceof Expr\New_ 116 | || $node instanceof Expr\Instanceof_ 117 | ) { 118 | if ($node->class instanceof Name) { 119 | $className = $node->class->toString(); 120 | $lowerClassName = strtolower($className); 121 | if ($lowerClassName != 'self' && $lowerClassName != 'parent' && $lowerClassName != 'parent') { 122 | $this->uses[$className] = true; 123 | } 124 | } 125 | } elseif ($node instanceof Stmt\Catch_) { 126 | foreach ($node->types as $type) { 127 | $this->uses[$type->toString()] = true; 128 | } 129 | } /*elseif ($node instanceof Expr\FuncCall) { 130 | if ($node->name instanceof Name) { 131 | echo $node->name->toString()."\n"; 132 | $this->uses[$node->name->toString()] = true; 133 | } 134 | }*/ /* elseif ($node instanceof Expr\ConstFetch) { 135 | $this->uses[$node->name->toString()] = true; 136 | }*/ elseif ($node instanceof Stmt\TraitUse) { 137 | foreach ($node->traits as $trait) { 138 | $this->uses[$trait->toString()] = true; 139 | } 140 | } elseif ($node instanceof Node\Param 141 | && $node->type instanceof Name 142 | ) { 143 | $this->uses[$node->type->toString()] = true; 144 | } 145 | } 146 | 147 | private function ensureUtf8($str) 148 | { 149 | if (preg_match('%^(?: 150 | [\x09\x0A\x0D\x20-\x7E] # ASCII 151 | | [\xC2-\xDF][\x80-\xBF] # non-overlong 2-byte 152 | | \xE0[\xA0-\xBF][\x80-\xBF] # excluding overlongs 153 | | [\xE1-\xEC\xEE\xEF][\x80-\xBF]{2} # straight 3-byte 154 | | \xED[\x80-\x9F][\x80-\xBF] # excluding surrogates 155 | | \xF0[\x90-\xBF][\x80-\xBF]{2} # planes 1-3 156 | | [\xF1-\xF3][\x80-\xBF]{3} # planes 4-15 157 | | \xF4[\x80-\x8F][\x80-\xBF]{2} # plane 16 158 | )*$%xs', $str)) { 159 | return $str; 160 | } else { 161 | return iconv('CP1252', 'UTF-8', $str); 162 | } 163 | } 164 | } 165 | -------------------------------------------------------------------------------- /src/Mouf/Packanalyst/Widgets/Node.php: -------------------------------------------------------------------------------- 1 | [0.1,0.2,0.3], 33 | * "packageName2"=>[0.1,0.2,0.3] 34 | * ]. 35 | * 36 | * @var array 37 | */ 38 | private $packages = []; 39 | 40 | private $packagesScores = []; 41 | 42 | public function __construct($className, $type) 43 | { 44 | $this->name = $className; 45 | $this->type = $type; 46 | } 47 | 48 | public function registerPackage($packageName, $version, $downloads, $favers) 49 | { 50 | if (!isset($this->packages[$packageName])) { 51 | $this->packages[$packageName] = []; 52 | $this->packagesScores[$packageName] = 1 + $downloads + $favers * 100; 53 | $this->score += $this->packagesScores[$packageName]; 54 | } 55 | 56 | if (array_search($version, $this->packages[$packageName]) === false) { 57 | $this->packages[$packageName][] = $version; 58 | } 59 | } 60 | 61 | /** 62 | * An array of arrays of important packages (the ones that have the higher score): 63 | * [ 64 | * "packageName"=>[0.1,0.2,0.3], 65 | * "packageName2"=>[0.1,0.2,0.3] 66 | * ]. 67 | * 68 | * @var array 69 | */ 70 | private $importantPackages = null; 71 | 72 | /** 73 | * An array of arrays of not so important packages (the ones that have the lower score): 74 | * [ 75 | * "packageName"=>[0.1,0.2,0.3], 76 | * "packageName2"=>[0.1,0.2,0.3] 77 | * ]. 78 | * 79 | * @var array 80 | */ 81 | private $notImportantPackages = null; 82 | 83 | public function getImportantPackages() 84 | { 85 | if ($this->importantPackages !== null) { 86 | return $this->importantPackages; 87 | } 88 | $this->sortPackages(); 89 | 90 | return $this->importantPackages; 91 | } 92 | 93 | public function getNotImportantPackages() 94 | { 95 | if ($this->notImportantPackages !== null) { 96 | return $this->notImportantPackages; 97 | } 98 | $this->sortPackages(); 99 | 100 | return $this->notImportantPackages; 101 | } 102 | 103 | private function sortPackages() 104 | { 105 | $this->importantPackages = []; 106 | $this->notImportantPackages = []; 107 | 108 | if (empty($this->packagesScores)) { 109 | return; 110 | } 111 | $maxScore = max($this->packagesScores); 112 | 113 | $threshold = (int) $maxScore / 100; 114 | 115 | foreach ($this->packages as $packageName => $versions) { 116 | if ($this->packagesScores[$packageName] >= $threshold) { 117 | $this->importantPackages[$packageName] = $versions; 118 | } else { 119 | $this->notImportantPackages[$packageName] = $versions; 120 | } 121 | } 122 | } 123 | 124 | public function addChild(Node $node) 125 | { 126 | if (array_search($node, $this->children) === false) { 127 | $this->children[] = $node; 128 | } 129 | } 130 | 131 | /** 132 | * The score of a node is the sum of the score of the packages implementing it. 133 | * 134 | * @return int 135 | */ 136 | public function getScore() 137 | { 138 | return $this->score; 139 | } 140 | 141 | /** 142 | * Returns the list of children, sorted by reverse score order. 143 | */ 144 | public function getChildrenSortedByScore() 145 | { 146 | usort($this->children, function ($a, $b) { 147 | return $b->getScore() - $a->getScore(); 148 | }); 149 | 150 | return $this->children; 151 | } 152 | 153 | public function getNbStars() 154 | { 155 | if ($this->score >= 1000000) { 156 | return 5; 157 | } elseif ($this->score >= 100000) { 158 | return 4; 159 | } elseif ($this->score >= 10000) { 160 | return 3; 161 | } elseif ($this->score >= 1000) { 162 | return 2; 163 | } elseif ($this->score >= 100) { 164 | return 1; 165 | } else { 166 | return 0; 167 | } 168 | } 169 | 170 | /** 171 | * Returns the depth of the node (0 if the node has no child). 172 | * 173 | * @return int 174 | */ 175 | public function getDepth() 176 | { 177 | if (empty($this->children)) { 178 | return 0; 179 | } else { 180 | return max(array_map(function ($item) { return $item->getDepth() + 1; }, $this->children)); 181 | } 182 | } 183 | 184 | private $reverseDepth = null; 185 | 186 | /** 187 | * Returns the depth of the node compared to the max depth of the parent (for reverse display). 188 | */ 189 | public function getRevertDepth() 190 | { 191 | if ($this->reverseDepth !== null) { 192 | return $this->reverseDepth; 193 | } 194 | $this->setReverseDepth($this->getDepth()); 195 | 196 | return $this->reverseDepth; 197 | } 198 | 199 | private function setReverseDepth($depth) 200 | { 201 | $this->reverseDepth = $depth; 202 | foreach ($this->children as $child) { 203 | $child->setReverseDepth($depth - 1); 204 | } 205 | } 206 | 207 | /** 208 | * Renders the tree, in reverse order! 209 | */ 210 | public function getHtmlRevert(): string 211 | { 212 | ob_start(); 213 | \Mouf::getDefaultRenderer()->render($this, 'revert'); 214 | 215 | return ob_get_clean(); 216 | } 217 | 218 | protected $replacementNode; 219 | 220 | /** 221 | * Replaces this node rendering with another HtmlElementInterface. 222 | * Used for the root node in htmlrevert mode. 223 | */ 224 | public function replaceNodeRenderingWith(HtmlElementInterface $graph): void 225 | { 226 | $this->replacementNode = $graph; 227 | } 228 | 229 | /** 230 | * @var bool 231 | */ 232 | protected $highlight = false; 233 | 234 | /** 235 | * Sets whether we should highlight or not the class (in yellow). 236 | */ 237 | public function setHighlight(bool $highlight): void 238 | { 239 | $this->highlight = $highlight; 240 | } 241 | } 242 | -------------------------------------------------------------------------------- /src/views/src/less/style.less: -------------------------------------------------------------------------------- 1 | 2 | // TODO : Change the @import into element. 3 | @import url(http://fonts.googleapis.com/css?family=Roboto:400,300,500,400italic); 4 | @import "variables.less"; 5 | @import "global.less"; 6 | @import "view-home.less"; 7 | @import "view-result.less"; 8 | 9 | 10 | 11 | /* Typeahead Bootstrap 3 styling */ 12 | .tt-dropdown-menu { 13 | position: absolute; 14 | top: 100%; 15 | left: 0; 16 | z-index: 1000; 17 | display: none; 18 | float: left; 19 | min-width: 160px; 20 | padding: 5px 0; 21 | margin: 2px 0 0; 22 | list-style: none; 23 | font-size: 14px; 24 | background-color: rgba(255,255,255, .95); 25 | border: 1px solid rgba(0, 0, 0, 0.15); 26 | -webkit-box-shadow: 0 6px 12px #b9b9b9; 27 | box-shadow: none; 28 | background-clip: padding-box; 29 | width: 100%; 30 | border-radius: 0; 31 | } 32 | .tt-suggestion { 33 | & > p { 34 | display: block; 35 | padding: 5px 20px; 36 | clear: both; 37 | font-weight: normal; 38 | color: #333333; 39 | white-space: nowrap; 40 | font-size: 18px; 41 | line-height: normal; 42 | } 43 | & > p:hover, 44 | & > p:focus, 45 | &.tt-cursor p { 46 | position: relative; 47 | background-color: #4181b7; 48 | color: #ffffff; 49 | text-decoration: none; 50 | outline: 0; 51 | cursor: pointer; 52 | .transition-duration(); 53 | } 54 | } 55 | 56 | .twitter-typeahead { width: 100%; } 57 | .nb-result { 58 | display: block; 59 | margin-bottom: 10px; 60 | } 61 | .item { 62 | background-color: #eaeaea; 63 | border: 1px solid lighten(#adadad, 15%); 64 | margin-bottom: 3px; 65 | padding: 10px 10px 10px 25px; 66 | background-repeat: no-repeat; 67 | background-position: 5px 15px; 68 | &.highlight,&.highlight.class,&.highlight.interface,&.highlight.trait,&.highlight.package { 69 | .gradient; 70 | box-shadow: inset 0px 1px 0 #5e9fd7; 71 | border: 1px solid #21527c; 72 | padding-left: 15px; 73 | a { 74 | color: @whiteColor; 75 | } 76 | .package { 77 | color: darken(@whiteColor, 15%); 78 | } 79 | .className { 80 | font-weight: @mediumWeight; 81 | font-size: 18px; 82 | } 83 | .package.small { 84 | overflow: hidden; 85 | &:nth-child(odd) { 86 | background: lighten(#326c9e, 4%); 87 | } 88 | } 89 | } 90 | &.item-result { 91 | padding: 0; 92 | .transition(); 93 | &:hover { 94 | background-color: darken(#eaeaea, 10%); 95 | a { 96 | text-decoration: none; 97 | } 98 | } 99 | .className { 100 | height: auto; 101 | padding-left: 25px; 102 | padding-right: 10px; 103 | a { 104 | display: block; 105 | padding: 10px 0; 106 | } 107 | } 108 | } 109 | &.class { 110 | background-image: url("images/class_obj.png"); 111 | } 112 | &.interface { 113 | background-image: url("images/int_obj.png"); 114 | } 115 | &.trait { 116 | background-image: url("images/trait_obj.png"); 117 | } 118 | &.package { 119 | background-image: url("images/package_obj.png"); 120 | } 121 | .package.small { 122 | overflow: hidden; 123 | &:nth-child(odd) { 124 | background: lighten(#EAEAEA, 4%); 125 | } 126 | } 127 | a { 128 | color: #333 129 | } 130 | a.otherpackageslink { 131 | font-size: 85%; 132 | font-weight: @mediumWeight; 133 | &:before { 134 | display: block; 135 | content: "\2b"; 136 | float: left; 137 | font-size: 17px; 138 | margin-right: 5px; 139 | } 140 | } 141 | .otherpackages { 142 | display: none; 143 | } 144 | .stars-container { 145 | float: right; 146 | padding: 3px; 147 | } 148 | } 149 | 150 | 151 | .className { 152 | height: 32px; 153 | overflow: hidden; 154 | } 155 | 156 | .badge { 157 | a { 158 | color: white 159 | } 160 | border-radius: 0; 161 | background: #004444; 162 | float: right; 163 | margin-right: 1px; 164 | font-weight: 300; 165 | color: #5e9fd7; 166 | opacity: .5; 167 | margin-top: 1px; 168 | } 169 | 170 | .versions .badge { 171 | font-size: 12px; 172 | } 173 | 174 | div.description { 175 | font-style: italic; 176 | } 177 | 178 | input.search-field.inputlg.form-control { 179 | font-size: 18px; 180 | color: #000; 181 | border-radius: 0; 182 | box-shadow: none; 183 | border: 1px solid #b9b9b9; 184 | height: auto; 185 | padding: 10px 15px; 186 | &:active, 187 | &:hover, 188 | &:focus { 189 | border-color: #000; 190 | box-shadow: none; 191 | } 192 | } 193 | 194 | .button-search { 195 | height: auto; 196 | padding: 10px 15px; 197 | color: #FFF; 198 | border-radius: 0; 199 | background: #629935; 200 | box-shadow: inset 0px 1px 0 #5e9fd7; 201 | font-size: 18px; 202 | border: 1px solid #21527c; 203 | text-shadow: 0 1px 0 rgba(0,0,0,.25); 204 | .gradient(); 205 | .transition-duration(); 206 | &:hover, 207 | &:active, 208 | &:focus { 209 | color: #FFF; 210 | border: 1px solid #21527c; 211 | box-shadow: inset 0px 1px 0 #5e9fd7; 212 | opacity: 0.95; 213 | outline: none; 214 | } 215 | &:focus { 216 | .gradient(darken(#2d6697, 10), darken(#2d6697, 10), darken(#4181b7, 10)); 217 | border: 1px solid darken(#21527c, 10); 218 | box-shadow: inset 0px 1px 0 darken(#5e9fd7, 10); 219 | outline: none; 220 | } 221 | } 222 | 223 | .footer { 224 | position: absolute; 225 | bottom: 0; 226 | width: 100%; 227 | } 228 | 229 | .panel-default, .panel { 230 | background: none; 231 | border: none; 232 | box-shadow: none; 233 | } 234 | 235 | .panel-default >.panel-heading { 236 | font-size: 18px; 237 | font-weight: @mediumWeight; 238 | border: none; 239 | position: relative; 240 | background: none; 241 | &:after { 242 | content: ''; 243 | display: block; 244 | float: left; 245 | height: 1px; 246 | width: 100%; 247 | background: rgb(0, 0, 0); 248 | top: 0px; 249 | bottom: 0px; 250 | margin: auto; 251 | position: absolute; 252 | } 253 | .ico-panel { 254 | float: left; 255 | position: absolute; 256 | left: 10px; 257 | top: 0; 258 | background: #FFF; 259 | z-index: 3; 260 | } 261 | h3 { 262 | margin: 0; 263 | text-align: left; 264 | font-weight: 300; 265 | padding-left: 55px; 266 | display: inline; 267 | background: #FFF; 268 | position: relative; 269 | z-index: 2; 270 | padding-right: 15px; 271 | } 272 | } 273 | .navbar-nav > li > a { 274 | border-top: 3px solid transparent; 275 | text-transform: uppercase; 276 | font-size: 14px; 277 | font-weight: normal; 278 | } 279 | .navbar-default .navbar-nav > .active > a, 280 | .navbar-default .navbar-nav > .active > a:hover, 281 | .navbar-default .navbar-nav > .active > a:focus { 282 | background: none; 283 | border-top: 3px solid #326c9e; 284 | color: @darkColor; 285 | } 286 | 287 | .btn-social { 288 | position: relative; 289 | padding-left: 44px; 290 | text-align: left; 291 | white-space: nowrap; 292 | overflow: hidden; 293 | text-overflow: ellipsis; 294 | font-weight: @lightWeight; 295 | background-repeat: no-repeat; 296 | background-position: 20px 10px; 297 | border-radius: 0; 298 | .transition(); 299 | opacity: .75; 300 | &:hover { 301 | opacity: 1; 302 | } 303 | &.btn-github { 304 | color: #fff; 305 | background-color: #2b2b2b; 306 | border-color: rgba(0,0,0,0.2); 307 | text-decoration: none; 308 | background-image: url('@{imagePath}github.png'); 309 | &:hover { 310 | background-color: darken(#2b2b2b, 10%); 311 | } 312 | } 313 | &.btn-twitter { 314 | color: #fff; 315 | background-color: #55acee; 316 | border-color: rgba(0,0,0,0.2); 317 | background-image: url('@{imagePath}twitter.png'); 318 | &:hover { 319 | background-color: darken(#55acee, 10%); 320 | } 321 | } 322 | &.btn-lg { 323 | padding: 10px 16px; 324 | font-size: 18px; 325 | line-height: 1.33; 326 | padding-left: 61px; 327 | } 328 | } -------------------------------------------------------------------------------- /src/Mouf/Packanalyst/Dao/ItemDao.php: -------------------------------------------------------------------------------- 1 | collection = $collection; 26 | $this->elasticSearchService = $elasticSearchService; 27 | } 28 | 29 | public function createIndex() 30 | { 31 | $this->collection->createIndex([ 32 | 'inherits' => 1, 33 | ]); 34 | $this->collection->createIndex([ 35 | 'globalInherits' => 1, 36 | ]); 37 | $this->collection->createIndex([ 38 | 'name' => 1, 39 | ]); 40 | $this->collection->createIndex([ 41 | 'uses' => 1, 42 | ]); 43 | $this->collection->createIndex([ 44 | 'packageName' => 1, 45 | 'packageVersion' => 1, 46 | ]); 47 | } 48 | 49 | /** 50 | * Drops the complete collection. 51 | */ 52 | public function drop() 53 | { 54 | $this->collection->drop(); 55 | } 56 | 57 | /** 58 | * Deletes all items relative to this package version. 59 | * 60 | * @param string $packageName 61 | * @param string $packageVersion 62 | */ 63 | public function deletePackage($packageName, $packageVersion) 64 | { 65 | $this->collection->deleteMany([ 66 | 'packageName' => $packageName, 67 | 'packageVersion' => $packageVersion, 68 | ]); 69 | } 70 | 71 | public function save($item) 72 | { 73 | //$this->collection->save($item); 74 | $this->recomputeGlobalInherits($item); 75 | 76 | // Let's store all possible class names in ElasticSearch. 77 | $this->elasticSearchService->storeItemName($item['name'], $item['type']); 78 | if (isset($item['inherits'])) { 79 | foreach ($item['inherits'] as $itemName) { 80 | $this->elasticSearchService->storeItemName($itemName, null); 81 | } 82 | } 83 | } 84 | 85 | /** 86 | * Returns the list of items whose name is $itemName. 87 | * 88 | * @param string $itemName 89 | * 90 | * @return \MongoDB\Driver\Cursor 91 | */ 92 | public function getItemsByName($itemName) 93 | { 94 | return $this->collection->find(['name' => $itemName]); 95 | } 96 | 97 | /** 98 | * For the item passed in parameter, compute the "globalInherits" array from the "inherits" array 99 | * and the "globalInherits" array of all inherited items, then, impact recursively all children 100 | * implementing this item. 101 | * 102 | * @param array $item 103 | * @param array $antiLoopList A list of already visited item names. 104 | */ 105 | protected function recomputeGlobalInherits(array $item, array &$antiLoopList = array()) 106 | { 107 | // Let's prevent any infinite loops. 108 | if (isset($antiLoopList[$item['name'].' '.$item['packageName'].' '.$item['packageVersion']])) { 109 | return; 110 | } 111 | 112 | $inherits = isset($item['inherits']) ? (array) $item['inherits'] : []; 113 | $globalInherits = $inherits ?: []; 114 | 115 | foreach ($inherits as $inheritedItemName) { 116 | foreach ($this->getItemsByName($inheritedItemName) as $parentItem) { 117 | $globalInherits = array_merge($globalInherits, isset($parentItem->globalInherits) ? (array) $parentItem->globalInherits : array()); 118 | } 119 | } 120 | 121 | // Let's remove duplicates 122 | $globalInherits = array_keys(array_flip($globalInherits)); 123 | 124 | $item['globalInherits'] = $globalInherits; 125 | 126 | $securedItem = self::ensureUtf8StringInArray($item); 127 | 128 | if (isset($securedItem['_id'])) { 129 | $this->collection->findOneAndReplace(['_id'=>$securedItem['_id']], $securedItem); 130 | } else { 131 | $this->collection->insertOne($securedItem); 132 | } 133 | 134 | 135 | $antiLoopList[$item['name'].' '.$item['packageName'].' '.$item['packageVersion']] = true; 136 | 137 | // Now, let's find the list of all items directly implementing this item 138 | $children = $this->collection->find(['inherits' => $item['name']]); 139 | foreach ($children as $child) { 140 | $childJson = (array) $child->jsonSerialize(); 141 | $this->recomputeGlobalInherits($childJson, $antiLoopList); 142 | } 143 | } 144 | 145 | /** 146 | * Find the list of items that inherit in a way or another $itemName. 147 | * 148 | * @param string $itemName 149 | */ 150 | public function findItemsInheriting($itemName, $limit = null) 151 | { 152 | $options = []; 153 | if ($limit !== null) { 154 | $options['limit'] = $limit; 155 | } 156 | return $this->collection->find(['globalInherits' => $itemName], $options); 157 | } 158 | 159 | /** 160 | * Find the list of items that use $itemName. 161 | * 162 | * @param string $itemName 163 | * @return array 164 | */ 165 | public function findItemsUsing($itemName, $limit = null) 166 | { 167 | $options = []; 168 | if ($limit !== null) { 169 | $options['limit'] = $limit; 170 | } 171 | return $this->collection->find(['uses' => $itemName], $options)->toArray(); 172 | } 173 | 174 | /** 175 | * Find the list of items that inherit in a way or another $itemName. 176 | */ 177 | public function findItemsByPackageVersion(string $packageName, string $packageVersion, array $options = []): Cursor 178 | { 179 | return $this->collection->find(['packageName' => $packageName, 'packageVersion' => $packageVersion], $options); 180 | } 181 | 182 | public function applyOnAllItemName(callable $callback) 183 | { 184 | foreach ($this->collection->find() as $item) { 185 | $callback((array) $item); 186 | } 187 | } 188 | 189 | /** 190 | * @param string $packageName 191 | */ 192 | public function findItemsByPackage($packageName) 193 | { 194 | return $this->collection->find(['packageName' => $packageName]); 195 | } 196 | 197 | public function applyScore($packageName, $score) 198 | { 199 | $this->collection->updateMany(['packageName' => $packageName], 200 | ['$set' => ['boost' => $score], 201 | ]); 202 | 203 | /*$items = $this->findItemsByPackage($packageName); 204 | 205 | foreach ($items as $item) { 206 | $this->collection->update(array( 207 | 'id'=>$item['_id'], 208 | 'body'=>[ 209 | 'boost'=>$score 210 | ], 211 | //'refresh' => true 212 | )); 213 | }*/ 214 | } 215 | 216 | /** 217 | * Ensures all elemets of an array are UTF8. 218 | * If not, convert to utf8 if possible. 219 | * 220 | * @param array $array 221 | */ 222 | private static function ensureUtf8StringInArray(array $array) 223 | { 224 | return self::array_map_recursive(function ($str) { 225 | if (!\is_string($str) || mb_check_encoding($str, 'UTF-8')) { 226 | return $str; 227 | } else { 228 | return utf8_encode($str); 229 | } 230 | }, $array); 231 | } 232 | 233 | private static function array_map_recursive($callback, $array) 234 | { 235 | foreach ($array as $key => $value) { 236 | if (is_array($array[$key])) { 237 | $array[$key] = self::array_map_recursive($callback, $array[$key]); 238 | } else { 239 | $array[$key] = call_user_func($callback, $array[$key]); 240 | } 241 | } 242 | 243 | return $array; 244 | } 245 | } 246 | -------------------------------------------------------------------------------- /src/views/root/index.php: -------------------------------------------------------------------------------- 1 | 2 | 30 | 31 | 32 | 33 | 64 | 65 |
    66 |
    67 |
    68 |
    69 | 70 |
    71 | 72 |

    Find any class implementing your interface

    73 |
    74 |
    75 |
    76 |

    Packanalyst can be useful for the average developer, but we believe it can be tremendously 77 | useful for any package developer. Indeed, using Packanalyst, you can find any package containing 78 | classes that implement/extend or simply use your classes/interfaces. 79 |

    80 |

    Therefore, this is an absolutely unique tool to know who is using and implementing 81 | your interfaces / abstract classes / traits. For instance, have a look at all the classes 82 | that implement the PSR3 LoggerInterface.

    83 |
    84 |
    85 |
    86 | 87 | 88 |
    89 | 90 |
    91 | 92 |

    Feedback needed!

    93 |
    94 |
    95 |
    96 |

    Packanalyst is a service in beta. Do not hesitate to send us feedback, or pull requests. 97 | Packanalyst is released in AGPL.

    98 | 99 | 111 |
    112 |
    113 |
    114 | 115 | 116 |
    117 |
    118 | 119 |

    How does it work?

    120 |
    121 |
    122 |
    123 |

    Packanalyst regularly scans the Packagist repository for new or updated PHP packages. Each package is 124 | analyzed and all classes interfaces and traits are extracted and stored in our database for later search. 125 |

    126 |
    127 | 128 |
    Do I need to do something special to register my package on Packanalyst?
    129 |
    No, you just need to register your package on Packagist and it will automatically be scanned 130 | by Packanalyst.
    131 | 132 |
    How long does it take for my package to be scanned?
    133 |
    Depending on the number of packages changed, it can take anything between an hour and a few days for 134 | your package to be analyzed after you register it or you make changes to it.
    135 | 136 |
    What versions of my package are scanned and stored?
    137 |
    For performance reason, Packanalyst does not scan all versions of your package. It will scan 138 | the master branch of your project and all latest tagged major versions.
    139 | 140 |
    What are those stars displayed next to some classes or interfaces?
    141 |
    In order to highlight the main classes used, we put in place a simple rating system. 142 | The goal is simply to highlight the classes that are most used. The rating system does not 143 | reflect the quality of the class, it simply reflects its usage. It is based on the number 144 | of downloads on Packagist: 145 |
      146 |
    • No stars: <100 downloads
    • 147 |
    • : Between 100 and 1000 downloads
    • 148 |
    • : Between 1000 and 10000 downloads
    • 149 |
    • : Between 10000 and 100000 downloads
    • 150 |
    • : Between 100000 and 1000000 downloads
    • 151 |
    • : > 1000000 downloads
    • 152 |
    153 | Also, one "star" on Packagist will be translated into 100 downloads for the rating system of Packanalyst. 154 |
    155 |
    156 |
    157 |
    158 |
    159 | 160 | 161 |
    162 | 163 |
    164 | 165 |

    API

    166 |
    167 |
    168 |
    169 |

    In progress! A REST API will be released to query Packanalyst and integrate Packanalyst with third-party 170 | programs. Mouf will be the first framework to get a native integration with Packanalyst.

    171 |
    172 |
    173 |
    174 | 175 | 176 |
    177 | 178 |
    179 | 180 |

    Who is behind Packanalyst?

    181 |
    182 |
    183 |
    184 |

    Packanalyst is a service developed by David Négrier who happens to be the 185 | lead developer of the Mouf framework. 186 | Mouf is a PHP framework based on dependency injection. The core idea of Mouf is to help bind classes and components 187 | developed by many developers together. For this vision to come true, we need a set of core interfaces 188 | (this is the work of the PHP-FIG group), and a tool to find classes implementing those common interfaces 189 | (hence the development of Packanalyst). 190 |

    191 |

    David is CTO of TheCodingMachine, a French 192 | IT company, who is kindly sponsoring Packanalyst's development and hosting.

    193 |

    The design and front-end part has been developed by Hugo Averty, project manager for TheCodingMachine.

    194 |

    195 | Mouf 196 | TheCodingMachine 197 |

    198 |
    199 |
    200 |
    201 |
    202 |
    203 |
    204 | -------------------------------------------------------------------------------- /src/Mouf/Packanalyst/Services/ElasticSearchService.php: -------------------------------------------------------------------------------- 1 | 15 | */ 16 | class ElasticSearchService 17 | { 18 | /** 19 | * @var ItemDao 20 | */ 21 | private $itemDao; 22 | 23 | /** 24 | * @var PackageDao 25 | */ 26 | private $packageDao; 27 | 28 | private $elasticSearchClient; 29 | 30 | public function __construct(Client $elasticSearchClient) 31 | { 32 | $this->elasticSearchClient = $elasticSearchClient; 33 | } 34 | 35 | /** 36 | * Reindex everything in elastic search. 37 | */ 38 | public function reindexAll() 39 | { 40 | $this->deleteIndex(); 41 | $this->createIndex(); 42 | $this->itemDao->applyOnAllItemName(function ($item) { 43 | echo 'Indexing '.$item['name']."\n"; 44 | $this->storeItemName($item['name'], $item['type']); 45 | }); 46 | $this->packageDao->applyOnAllPackages(function ($item) { 47 | if (isset($item['packageName'])) { 48 | echo 'Indexing '.$item['packageName']."\n"; 49 | $this->storeItemName($item['packageName'], 'package'); 50 | } 51 | }); 52 | } 53 | 54 | /** 55 | * Delete index. 56 | */ 57 | public function deleteIndex() 58 | { 59 | try { 60 | $deleteParams = array(); 61 | $deleteParams['index'] = 'packanalyst'; 62 | $this->elasticSearchClient->indices()->delete($deleteParams); 63 | } catch (\Elasticsearch\Common\Exceptions\Missing404Exception $e) { //Elasticsearch\Common\Exceptions\Missing404Exception $ex) { 64 | // Ignore 404: if the index does not exist, it's ok. 65 | } 66 | } 67 | 68 | /** 69 | * Create index. 70 | */ 71 | public function createIndex() 72 | { 73 | $indexParams = array(); 74 | $indexParams['index'] = 'packanalyst'; 75 | //$indexParams['body']['settings']['number_of_shards'] = 2; 76 | //$indexParams['body']['settings']['number_of_replicas'] = 0; 77 | 78 | // Mapping as both a suggester and a not_analyzed field (to be searchable via filters) 79 | /*$itemNameMapping = array( 80 | 'properties' => array( 81 | "name" => [ "type" => "multi_field", 82 | "fields" => [ 83 | "name" => ["type" => "string"], 84 | "untouched" => ["type" => "string", "index" => "not_analyzed"] 85 | ], "suggest" => [ 86 | "type" => "completion", 87 | "index_analyzer" => "simple", 88 | "search_analyzer" => "simple", 89 | //"payloads" => true 90 | ]], 91 | ) 92 | );*/ 93 | $itemNameMapping = array( 94 | 'properties' => array( 95 | 'name' => [ 96 | 'type' => 'string', 97 | 'index' => 'not_analyzed' 98 | /*'fields' => [ 99 | 'raw' => ['type' => 'string', 'index' => 'not_analyzed'], 100 | ],*/ 101 | ], 102 | 'suggest' => [ 103 | 'type' => 'completion', 104 | // For older versions of Elasticsearch <=1.6) 105 | //'index_analyzer' => 'simple', 106 | // For newer versions of Elasticsearch 107 | 'analyzer' => 'simple', 108 | 109 | 'search_analyzer' => 'simple', 110 | //"payloads" => true 111 | ], 112 | 'boost' => [ 113 | 'type' => 'float', 114 | 'index' => 'not_analyzed', 115 | ], 116 | ), 117 | ); 118 | 119 | $indexParams['body']['mappings']['itemname'] = $itemNameMapping; 120 | 121 | $this->elasticSearchClient->indices()->create($indexParams); 122 | } 123 | 124 | /** 125 | * Stores an item name in elastic search. 126 | */ 127 | public function storeItemName(string $itemName, $type = null, ?float $boost = null) 128 | { 129 | 130 | // TODO: a local "cache" array that contain all the classes we know that exists in ElasticSearch. 131 | // When the local cache is set, remove the "refresh"=>true 132 | 133 | // Before inserting itemName, let's make sure it is not ALREADY in the index. 134 | $oldSource = $this->checkItemNameExists($itemName); 135 | if ($oldSource != false) { 136 | // item exists. 137 | if ($type != null && $oldSource['_source']['type'] != $type) { 138 | // We can update the type with the new type 139 | $doc = []; 140 | if ($type) { 141 | $doc['type'] = $type; 142 | } 143 | if ($boost) { 144 | $doc['boost'] = $boost; 145 | } 146 | 147 | $this->elasticSearchClient->update(array( 148 | 'id' => $oldSource['_id'], 149 | 'index' => 'packanalyst', 150 | 'type' => 'itemname', 151 | 'body' => [ 152 | 'doc' => $doc, 153 | ], 154 | 'refresh' => true, 155 | )); 156 | } 157 | 158 | return; 159 | } 160 | 161 | $store = explode('\\', $itemName); 162 | if (count($store) != 1) { 163 | $store[] = $itemName; 164 | } 165 | 166 | if ($boost == null) { 167 | $boost = 1.0; 168 | } 169 | 170 | $params = array(); 171 | $params['body'] = array( 172 | 'name' => $itemName, 173 | 'type' => $type, 174 | 'suggest' => [ 175 | 'input' => $store, 176 | 'output' => $itemName, 177 | ], 178 | 'boost' => $boost, 179 | ); 180 | $params['index'] = 'packanalyst'; 181 | $params['type'] = 'itemname'; 182 | $params['refresh'] = true; 183 | $ret = $this->elasticSearchClient->index($params); 184 | } 185 | 186 | public function deleteItemName($itemName) 187 | { 188 | $source = $this->checkItemNameExists($itemName); 189 | if ($source != false) { 190 | $this->elasticSearchClient->delete(array( 191 | 'id' => $source['_id'], 192 | 'index' => 'packanalyst', 193 | 'type' => 'itemname', 194 | )); 195 | } 196 | } 197 | 198 | /** 199 | * False if the item name does not exist, or the source if the type does exist. 200 | * 201 | */ 202 | private function checkItemNameExists(string $itemName) 203 | { 204 | $params = array(); 205 | $params['body'] = [ 206 | 'filter' => [ 207 | 'term' => [ 208 | 'name' => $itemName, 209 | ], 210 | ], 211 | ]; 212 | $params['index'] = 'packanalyst'; 213 | $params['type'] = 'itemname'; 214 | 215 | try { 216 | $ret = $this->elasticSearchClient->search($params); 217 | } catch (ServerErrorResponseException $e) { 218 | // Note: it seems an error is triggered if the index is empty. 219 | error_log('Exception in search: '.$e->getMessage()."\n".$e->getTraceAsString()); 220 | return; 221 | } 222 | 223 | if ($ret['hits']['total'] == 0) { 224 | return false; 225 | } else { 226 | if (isset($ret['hits']['hits'][0])) { 227 | return $ret['hits']['hits'][0]; 228 | } else { 229 | return; 230 | } 231 | } 232 | } 233 | 234 | public function suggestItemName($input, $size = 10) 235 | { 236 | $params = [ 237 | 'body' => [ 238 | 'itemname' => [ 239 | 'text' => $input, 240 | 'completion' => [ 241 | 'field' => 'suggest', 242 | 'fuzzy' => true, 243 | 'size' => $size, 244 | ], 245 | ], 246 | ], 247 | ]; 248 | 249 | $suggestions = $this->elasticSearchClient->suggest($params); 250 | 251 | if (!isset($suggestions['itemname'])) { 252 | throw new \Exception('Error while querying autocomplete: '.json_encode($suggestions)); 253 | } 254 | 255 | return array_map(function ($item) { 256 | return [ 257 | 'value' => $item['text'], 258 | ]; 259 | }, $suggestions['itemname'][0]['options']); 260 | } 261 | 262 | /** 263 | * @return array(["total"=>xxx, "max_score"=>xxxx, hits=>[]]) 264 | */ 265 | public function suggestItemName2(string $input, int $size = 10, int $offset = 0) 266 | { 267 | $params = 268 | [ 269 | 'body' => [ 270 | 'query' => [ 271 | 'function_score' => [ 272 | 'query' => [ 273 | 'bool' => [ 274 | 'should' => [ 275 | [ 276 | 'wildcard' => [ 277 | 'name' => '*'.addslashes($input).'*', 278 | ], 279 | ], 280 | [ 281 | 'match' => [ 282 | 'suggest' => [ 283 | 'query' => $input, 284 | 'fuzziness' => 'AUTO', 285 | ], 286 | 287 | ], 288 | ] 289 | ], 290 | ], 291 | ], 292 | 'functions' => [ 293 | [ 294 | 'field_value_factor' => [ 295 | 'field' => 'boost', 296 | ], 297 | ], 298 | ], 299 | 'score_mode' => 'multiply', 300 | ], 301 | ], 302 | ], 303 | 'size' => $size + $offset, 304 | 305 | ]; 306 | 307 | $suggestions = $this->elasticSearchClient->search($params); 308 | 309 | //var_dump($suggestions);exit; 310 | 311 | if (!isset($suggestions['hits'])) { 312 | throw new \Exception('Error while querying search: '.json_encode($suggestions)); 313 | } 314 | 315 | $hits = $suggestions['hits']['hits']; 316 | 317 | for ($i = 0; $i < $offset; ++$i) { 318 | array_shift($hits); 319 | } 320 | 321 | $suggestions['hits']['hits'] = $hits; 322 | 323 | return $suggestions['hits']; 324 | } 325 | 326 | public function setItemDao(ItemDao $itemDao) 327 | { 328 | $this->itemDao = $itemDao; 329 | 330 | return $this; 331 | } 332 | 333 | public function setPackageDao(PackageDao $packageDao) 334 | { 335 | $this->packageDao = $packageDao; 336 | 337 | return $this; 338 | } 339 | } 340 | -------------------------------------------------------------------------------- /src/Mouf/Packanalyst/Services/FetchDataService.php: -------------------------------------------------------------------------------- 1 | 21 | */ 22 | class FetchDataService 23 | { 24 | /** 25 | * @var ComposerRepository 26 | */ 27 | private $packagistRepository; 28 | 29 | private $logger; 30 | private $itemDao; 31 | private $packageDao; 32 | 33 | /** 34 | * If set, this is the only package that will be forced. 35 | * 36 | * @var string 37 | */ 38 | private $forcedPackage; 39 | 40 | /** 41 | * @var bool 42 | */ 43 | private $retryOnError; 44 | 45 | /** 46 | * @var bool 47 | */ 48 | private $force; 49 | 50 | /** 51 | * @var DownloadManager 52 | */ 53 | private $downloadManager; 54 | 55 | /** 56 | * @var ClassesDetector 57 | */ 58 | private $classesDetector; 59 | 60 | public function __construct(ClassesDetector $classesDetector, LoggerInterface $logger, ItemDao $itemDao, PackageDao $packageDao) 61 | { 62 | $this->classesDetector = $classesDetector; 63 | $this->logger = $logger; 64 | $this->itemDao = $itemDao; 65 | $this->packageDao = $packageDao; 66 | } 67 | 68 | /** 69 | * @param ComposerRepository $packagistRepository 70 | */ 71 | public function setPackagistRepository(ComposerRepository $packagistRepository) 72 | { 73 | $this->packagistRepository = $packagistRepository; 74 | 75 | return $this; 76 | } 77 | 78 | /** 79 | * @param DownloadManager $downloadManager 80 | */ 81 | public function setDownloadManager(DownloadManager $downloadManager) 82 | { 83 | $this->downloadManager = $downloadManager; 84 | 85 | return $this; 86 | } 87 | 88 | /** 89 | * Forces to download only one package. 90 | * 91 | * @param string $forcedPackage 92 | */ 93 | public function setForcedPackage($forcedPackage) 94 | { 95 | $this->forcedPackage = $forcedPackage; 96 | } 97 | 98 | /** 99 | * If set, any package in error will be retried.po. 100 | * 101 | * @param bool $retryOnError 102 | */ 103 | public function setRetryOnError($retryOnError) 104 | { 105 | $this->retryOnError = $retryOnError; 106 | } 107 | 108 | /** 109 | * Set whether package upload must be forced or not. 110 | * 111 | * @param bool $force 112 | */ 113 | public function setForce($force) 114 | { 115 | $this->force = $force; 116 | } 117 | 118 | /** 119 | * Runs the script: connect to packagist, download everything it can! 120 | */ 121 | public function run() 122 | { 123 | // TODO: each package is there twice. Find why. 124 | $filesystem = new Filesystem(); 125 | 126 | if (file_exists(DOWNLOAD_DIR.'/last_analyzed_package')) { 127 | $lastAnalyzedPackage = file_get_contents(DOWNLOAD_DIR.'/last_analyzed_package'); 128 | } else { 129 | $lastAnalyzedPackage = ''; 130 | } 131 | 132 | $providerNames = $this->packagistRepository->getProviderNames(); 133 | 134 | // If analyzis is over, let's start from the beginning again. 135 | if ($lastAnalyzedPackage >= $providerNames[count($providerNames) - 1]) { 136 | $lastAnalyzedPackage = ''; 137 | } 138 | 139 | $this->logger->debug('Starting script.'); 140 | $found = false; 141 | 142 | sort($providerNames); 143 | 144 | foreach ($providerNames as $packageName) { 145 | if ($this->forcedPackage && $this->forcedPackage != $packageName) { 146 | continue; 147 | } else { 148 | $found = true; 149 | } 150 | 151 | if (!$this->forcedPackage && $packageName <= $lastAnalyzedPackage) { 152 | continue; 153 | } 154 | 155 | // Let's write the name of the last package we are going to analyze 156 | // We will use it to start again from next package in case this package fails. 157 | if (!$this->forcedPackage) { 158 | file_put_contents(DOWNLOAD_DIR.'/last_analyzed_package', $packageName); 159 | } 160 | 161 | $this->logger->debug('Analyzing {packageName}.', array( 162 | 'packageName' => $packageName, 163 | )); 164 | 165 | //if ($packageName != '10up/wp_mock') continue; 166 | //var_dump($packagistRepo->findPackages($packageName)); 167 | 168 | $packages = $this->packagistRepository->findPackages($packageName); 169 | 170 | $packages = array_filter($packages, function ($package) use ($packageName) { return $package->getName() == $packageName; }); 171 | 172 | // Warning: findPackages uses "whatProvides". For instance: symfony/symonfy provides symfony/finder. 173 | // When a new version of finder is released, we don't want to remove symfony. Therefore, we need to restrict 174 | // the packages returned by "findPackages" 175 | $importantPackages = $this->getImportantVersions($packages); 176 | 177 | // DELETE PACKAGES VERSION BEFORE REINSERTION! 178 | 179 | // Only delete packages that are not important anymore. 180 | $notImportantPackages = array_diff($packages, $importantPackages); 181 | foreach ($notImportantPackages as $notImportantPackage) { 182 | if ($this->packageDao->get($notImportantPackage->getName(), $notImportantPackage->getPrettyVersion())) { 183 | $this->logger->info('Removing {packageName} {version}. A newer package is available.', array( 184 | 'packageName' => $notImportantPackage->getPrettyName(), 185 | 'version' => $notImportantPackage->getPrettyVersion(), 186 | )); 187 | 188 | $this->itemDao->deletePackage($notImportantPackage->getName(), $notImportantPackage->getPrettyVersion()); 189 | $this->packageDao->deletePackage($notImportantPackage->getName(), $notImportantPackage->getPrettyVersion()); 190 | 191 | $downloadPath = DOWNLOAD_DIR.'/'.$notImportantPackage->getName().'/'.$notImportantPackage->getPrettyVersion(); 192 | $filesystem->removeDirectory($downloadPath); 193 | } 194 | } 195 | 196 | foreach ($importantPackages as $package) { 197 | /* @var $package PackageInterface */ 198 | $packageVersion = null; 199 | try { 200 | // Let's reset to null (in case an exception happens on first line). 201 | $packageVersionEntity = null; 202 | 203 | // Let's get the update date of each version and let's compare it with the one we stored. 204 | $packageVersion = $this->packageDao->get($package->getName(), $package->getPrettyVersion()); 205 | 206 | if (!$this->force && ((!isset($packageVersion['refresh']) || !$packageVersion['refresh']))) { 207 | if ($packageVersion && $packageVersion['releaseDate']->toDateTime()->getTimestamp() == $package->getReleaseDate()->getTimestamp()) { 208 | if (isset($packageVersion['onError'])) { 209 | if ($packageVersion['onError'] == false || ($packageVersion['onError'] == true && !$this->retryOnError)) { 210 | $this->logger->debug('{packageName} {version} has not moved since last run. Ignoring.', array( 211 | 'packageName' => $package->getPrettyName(), 212 | 'version' => $package->getPrettyVersion(), 213 | )); 214 | continue; 215 | } 216 | } 217 | } 218 | } 219 | 220 | $this->itemDao->deletePackage($package->getName(), $package->getPrettyVersion()); 221 | $this->packageDao->deletePackage($package->getName(), $package->getPrettyVersion()); 222 | 223 | $this->logger->info('Downloading {packageName} {version}{additional}', array( 224 | 'packageName' => $package->getPrettyName(), 225 | 'version' => $package->getPrettyVersion(), 226 | 'additional' => ((isset($packageVersion['refresh']) && $packageVersion['refresh']) ? ' (forced via force-refresh)' : ''), 227 | )); 228 | //var_dump($package->getDistUrls()); 229 | //var_dump($package->getSourceUrls()); 230 | $downloadPath = DOWNLOAD_DIR.'/'.$package->getName().'/'.$package->getPrettyVersion(); 231 | 232 | $this->downloadManager->download($package, $downloadPath); 233 | 234 | $packageVersion = $this->packageDao->createOrUpdatePackage($package); 235 | 236 | $this->classesDetector->storePackage($downloadPath, $packageVersion); 237 | $packageVersion['onError'] = false; 238 | $packageVersion['errorMsg'] = ''; 239 | unset($packageVersion['refresh']); 240 | } catch (\Exception $e) { 241 | if (!$packageVersion) { 242 | $packageVersion = $this->packageDao->createOrUpdatePackage($package); 243 | } 244 | $this->logger->error('Package {packageName} {version} failed to download. Exception: '.$e->getMessage().' - '.$e->getTraceAsString(), 245 | array( 246 | 'packageName' => $package->getName(), 247 | 'version' => $package->getPrettyVersion(), 248 | 'exception' => $e, 249 | ) 250 | ); 251 | $packageVersion['packageName'] = $package->getName(); 252 | $packageVersion['packageVersion'] = $package->getPrettyVersion(); 253 | $packageVersion['onError'] = true; 254 | $packageVersion['errorMsg'] = $e->getMessage()."\n".$e->getTraceAsString(); 255 | } 256 | $this->packageDao->save($packageVersion); 257 | } 258 | } 259 | 260 | if ($this->forcedPackage && !$found) { 261 | $this->logger->error("Unable to find package '{packageName}'", ['packageName' => $this->forcedPackage]); 262 | } 263 | 264 | //var_dump("Nb packages: ".count($repositories->getPackages())); 265 | } 266 | 267 | /** 268 | * From an array of packages: 269 | * Analyze the list of packages. 270 | * Select: "master" + the latest version of each major version. 271 | * 272 | * @param Package[] $packages 273 | * 274 | * @return Package[] 275 | */ 276 | private function getImportantVersions(array $packages) 277 | { 278 | $indexedByVersion = []; 279 | foreach ($packages as $package) { 280 | // Let's ignore aliases. 281 | if ($package instanceof AliasPackage) { 282 | continue; 283 | } 284 | // Let's index by version, but let's first remove the first 'v' (like v1.0.0) 285 | $indexedByVersion[$package->getVersion()] = $package; 286 | } 287 | 288 | uksort($indexedByVersion, 'version_compare'); 289 | $indexedByVersion = array_reverse($indexedByVersion); 290 | 291 | $keptPackages = array(); 292 | 293 | $lastMajorVersion = -1; 294 | 295 | foreach ($indexedByVersion as $version => $package) { 296 | if ($version == '9999999-dev') { 297 | $keptPackages[] = $package; 298 | continue; 299 | } 300 | 301 | $versionItems = explode('.', $version); 302 | if (isset($versionItems[0]) && is_numeric($versionItems[0]) && $versionItems[0] != $lastMajorVersion) { 303 | $lastMajorVersion = $versionItems[0]; 304 | $keptPackages[] = $package; 305 | } 306 | } 307 | 308 | // If no package has been kept, let's grab all of them... 309 | if (empty($keptPackages)) { 310 | // But still, let's remove any AliasPackage 311 | $keptPackages = array_filter($packages, function ($package) { return !($package instanceof AliasPackage); }); 312 | } 313 | 314 | return $keptPackages; 315 | } 316 | } 317 | -------------------------------------------------------------------------------- /src/views/css/styles.css: -------------------------------------------------------------------------------- 1 | @import url(http://fonts.googleapis.com/css?family=Roboto:400,300,500,400italic); 2 | html, 3 | body { 4 | min-height: 100%; 5 | position: relative; 6 | } 7 | body { 8 | position: relative; 9 | font-family: 'Roboto', sans-serif; 10 | font-size: 16px; 11 | font-weight: 300; 12 | } 13 | strong, 14 | b, 15 | dt { 16 | font-weight: 500; 17 | } 18 | table.table { 19 | border: 1px solid #ddd; 20 | margin-top: 10px; 21 | } 22 | a { 23 | color: #386E9D; 24 | } 25 | th { 26 | font-weight: 500; 27 | } 28 | p.sub-title { 29 | font-size: 20px; 30 | } 31 | .well { 32 | border-radius: 0; 33 | background: none; 34 | } 35 | .well p { 36 | margin: 0; 37 | } 38 | .navbar-default { 39 | background: #ffffff url('images/bg-head.jpg') repeat-x top right; 40 | border: none; 41 | } 42 | .navbar-right { 43 | margin-right: 0px; 44 | } 45 | h2, 46 | h3, 47 | h4, 48 | h5 { 49 | font-weight: 300; 50 | } 51 | h1 { 52 | margin-top: 100px; 53 | display: block; 54 | font-size: 35px; 55 | margin-bottom: 20px; 56 | padding-left: 15px; 57 | font-weight: 300; 58 | } 59 | h1.logo { 60 | margin-top: 65px; 61 | font-weight: 400; 62 | } 63 | h1.small-logo { 64 | margin: 10px 0 10px 0; 65 | font-size: 25px; 66 | float: left; 67 | width: 210x; 68 | font-weight: 400; 69 | } 70 | h1.small-logo img { 71 | width: 30px; 72 | } 73 | h1.small-logo small { 74 | font-size: 12px; 75 | } 76 | h1 span.logo { 77 | font-weight: 500; 78 | color: #333333; 79 | } 80 | h1 span.blue { 81 | font-weight: 300; 82 | color: #2F699A; 83 | } 84 | h1 small { 85 | display: block; 86 | font-size: 16px; 87 | font-weight: 300; 88 | } 89 | h1 img { 90 | margin-right: 10px; 91 | } 92 | h1 a:hover { 93 | text-decoration: none; 94 | } 95 | .search-header { 96 | padding: 10px; 97 | } 98 | .search-header .search-field { 99 | width: 100%; 100 | } 101 | .search-header .twitter-typeahead { 102 | float: left; 103 | display: block !important; 104 | margin-right: 20px; 105 | } 106 | #search { 107 | background: #ffffff url('images/bg-head.jpg') repeat-x top right; 108 | margin-bottom: 20px; 109 | } 110 | .light { 111 | font-weight: 300; 112 | } 113 | .medium { 114 | font-weight: 500; 115 | } 116 | .jumbotron { 117 | background: none; 118 | } 119 | .item .stars-container { 120 | float: right; 121 | padding: 3px; 122 | } 123 | .pad { 124 | padding-left: 40px; 125 | } 126 | .pad.arrow { 127 | background: url("images/arrow.png") no-repeat; 128 | background-position: 20px 8px; 129 | position: relative; 130 | } 131 | .pad.arrow:before { 132 | content: ""; 133 | position: absolute; 134 | width: 0px; 135 | height: 100%; 136 | left: 20px; 137 | border-left: 1px dashed #100; 138 | } 139 | .pad.arrow:last-child:before { 140 | height: 20px; 141 | } 142 | #content > .pad.arrow:last-child:before { 143 | height: 20px; 144 | } 145 | .highlight { 146 | background-color: #ffff99; 147 | } 148 | ul.classgraph { 149 | padding-left: 0px; 150 | list-style-type: none; 151 | } 152 | ul.classgraph > li ul li { 153 | position: relative; 154 | } 155 | ul.classgraph > li ul li:before { 156 | content: ""; 157 | position: absolute; 158 | width: 0px; 159 | height: 100%; 160 | left: -20px; 161 | border-left: 1px dashed #100; 162 | } 163 | ul.classgraph > li ul li:last-child:before { 164 | height: 20px; 165 | } 166 | ul.classgraph ul { 167 | list-style-image: url("images/arrow.png"); 168 | } 169 | .githublink { 170 | -webkit-transition: all 0.2s ease-out; 171 | -moz-transition: all 0.2s ease-out; 172 | -o-transition: all 0.2s ease-out; 173 | transition: all 0.2s ease-out; 174 | padding-left: 40px; 175 | background-image: url("images/github-small.png"); 176 | background-position: 5px center; 177 | background-repeat: no-repeat; 178 | min-height: 20px; 179 | display: inline-block; 180 | border-radius: 0; 181 | border-color: #151515; 182 | } 183 | /* Typeahead Bootstrap 3 styling */ 184 | .tt-dropdown-menu { 185 | position: absolute; 186 | top: 100%; 187 | left: 0; 188 | z-index: 1000; 189 | display: none; 190 | float: left; 191 | min-width: 160px; 192 | padding: 5px 0; 193 | margin: 2px 0 0; 194 | list-style: none; 195 | font-size: 14px; 196 | background-color: rgba(255, 255, 255, 0.95); 197 | border: 1px solid rgba(0, 0, 0, 0.15); 198 | -webkit-box-shadow: 0 6px 12px #b9b9b9; 199 | box-shadow: none; 200 | background-clip: padding-box; 201 | width: 100%; 202 | border-radius: 0; 203 | } 204 | .tt-suggestion > p { 205 | display: block; 206 | padding: 5px 20px; 207 | clear: both; 208 | font-weight: normal; 209 | color: #333333; 210 | white-space: nowrap; 211 | font-size: 18px; 212 | line-height: normal; 213 | } 214 | .tt-suggestion > p:hover, 215 | .tt-suggestion > p:focus, 216 | .tt-suggestion.tt-cursor p { 217 | position: relative; 218 | background-color: #4181b7; 219 | color: #ffffff; 220 | text-decoration: none; 221 | outline: 0; 222 | cursor: pointer; 223 | -moz-transition-duration: 0.2s; 224 | -webkit-transition-duration: 0.2s; 225 | -o-transition-duration: 0.2s; 226 | transition-duration: 0.2s; 227 | } 228 | .twitter-typeahead { 229 | width: 100%; 230 | } 231 | .nb-result { 232 | display: block; 233 | margin-bottom: 10px; 234 | } 235 | .item { 236 | background-color: #eaeaea; 237 | border: 1px solid #d3d3d3; 238 | margin-bottom: 3px; 239 | padding: 10px 10px 10px 25px; 240 | background-repeat: no-repeat; 241 | background-position: 5px 15px; 242 | } 243 | .item.highlight, 244 | .item.highlight.class, 245 | .item.highlight.interface, 246 | .item.highlight.trait, 247 | .item.highlight.package { 248 | background: #2d6697; 249 | background: -webkit-gradient(linear, left bottom, left top, color-stop(0, #2d6697), color-stop(1, #4181b7)); 250 | background: -ms-linear-gradient(bottom, #2d6697, #4181b7); 251 | background: -moz-linear-gradient(center bottom, #2d6697 0%, #4181b7 100%); 252 | background: -o-linear-gradient(#4181b7, #2d6697); 253 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#4181b7', endColorstr='#2d6697', GradientType=0); 254 | box-shadow: inset 0px 1px 0 #5e9fd7; 255 | border: 1px solid #21527c; 256 | padding-left: 15px; 257 | } 258 | .item.highlight a, 259 | .item.highlight.class a, 260 | .item.highlight.interface a, 261 | .item.highlight.trait a, 262 | .item.highlight.package a { 263 | color: #ffffff; 264 | } 265 | .item.highlight .package, 266 | .item.highlight.class .package, 267 | .item.highlight.interface .package, 268 | .item.highlight.trait .package, 269 | .item.highlight.package .package { 270 | color: #d9d9d9; 271 | } 272 | .item.highlight .className, 273 | .item.highlight.class .className, 274 | .item.highlight.interface .className, 275 | .item.highlight.trait .className, 276 | .item.highlight.package .className { 277 | font-weight: 500; 278 | font-size: 18px; 279 | } 280 | .item.highlight .package.small, 281 | .item.highlight.class .package.small, 282 | .item.highlight.interface .package.small, 283 | .item.highlight.trait .package.small, 284 | .item.highlight.package .package.small { 285 | overflow: hidden; 286 | } 287 | .item.highlight .package.small:nth-child(odd), 288 | .item.highlight.class .package.small:nth-child(odd), 289 | .item.highlight.interface .package.small:nth-child(odd), 290 | .item.highlight.trait .package.small:nth-child(odd), 291 | .item.highlight.package .package.small:nth-child(odd) { 292 | background: #3777ad; 293 | } 294 | .item.item-result { 295 | padding: 0; 296 | -webkit-transition: all 0.2s ease-out; 297 | -moz-transition: all 0.2s ease-out; 298 | -o-transition: all 0.2s ease-out; 299 | transition: all 0.2s ease-out; 300 | } 301 | .item.item-result:hover { 302 | background-color: #d1d1d1; 303 | } 304 | .item.item-result:hover a { 305 | text-decoration: none; 306 | } 307 | .item.item-result .className { 308 | height: auto; 309 | padding-left: 25px; 310 | padding-right: 10px; 311 | } 312 | .item.item-result .className a { 313 | display: block; 314 | padding: 10px 0; 315 | } 316 | .item.class { 317 | background-image: url("images/class_obj.png"); 318 | } 319 | .item.interface { 320 | background-image: url("images/int_obj.png"); 321 | } 322 | .item.trait { 323 | background-image: url("images/trait_obj.png"); 324 | } 325 | .item.package { 326 | background-image: url("images/package_obj.png"); 327 | } 328 | .item .package.small { 329 | overflow: hidden; 330 | } 331 | .item .package.small:nth-child(odd) { 332 | background: #f4f4f4; 333 | } 334 | .item a { 335 | color: #333333; 336 | } 337 | .item a.otherpackageslink { 338 | font-size: 85%; 339 | font-weight: 500; 340 | } 341 | .item a.otherpackageslink:before { 342 | display: block; 343 | content: "\2b"; 344 | float: left; 345 | font-size: 17px; 346 | margin-right: 5px; 347 | } 348 | .item .otherpackages { 349 | display: none; 350 | } 351 | .item .stars-container { 352 | float: right; 353 | padding: 3px; 354 | } 355 | .className { 356 | height: 32px; 357 | } 358 | .badge { 359 | border-radius: 0; 360 | background: #004444; 361 | float: right; 362 | margin-right: 1px; 363 | font-weight: 300; 364 | color: #5e9fd7; 365 | opacity: .5; 366 | margin-top: 1px; 367 | } 368 | .badge a { 369 | color: #ffffff; 370 | } 371 | .versions .badge { 372 | font-size: 12px; 373 | } 374 | div.description { 375 | font-style: italic; 376 | } 377 | input.search-field.inputlg.form-control { 378 | font-size: 18px; 379 | color: #000; 380 | border-radius: 0; 381 | box-shadow: none; 382 | border: 1px solid #b9b9b9; 383 | height: auto; 384 | padding: 10px 15px; 385 | } 386 | input.search-field.inputlg.form-control:active, 387 | input.search-field.inputlg.form-control:hover, 388 | input.search-field.inputlg.form-control:focus { 389 | border-color: #000; 390 | box-shadow: none; 391 | } 392 | .button-search { 393 | height: auto; 394 | padding: 10px 15px; 395 | color: #FFF; 396 | border-radius: 0; 397 | background: #629935; 398 | box-shadow: inset 0px 1px 0 #5e9fd7; 399 | font-size: 18px; 400 | border: 1px solid #21527c; 401 | text-shadow: 0 1px 0 rgba(0, 0, 0, 0.25); 402 | background: #2d6697; 403 | background: -webkit-gradient(linear, left bottom, left top, color-stop(0, #2d6697), color-stop(1, #4181b7)); 404 | background: -ms-linear-gradient(bottom, #2d6697, #4181b7); 405 | background: -moz-linear-gradient(center bottom, #2d6697 0%, #4181b7 100%); 406 | background: -o-linear-gradient(#4181b7, #2d6697); 407 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#4181b7', endColorstr='#2d6697', GradientType=0); 408 | -moz-transition-duration: 0.2s; 409 | -webkit-transition-duration: 0.2s; 410 | -o-transition-duration: 0.2s; 411 | transition-duration: 0.2s; 412 | } 413 | .button-search:hover, 414 | .button-search:active, 415 | .button-search:focus { 416 | color: #FFF; 417 | border: 1px solid #21527c; 418 | box-shadow: inset 0px 1px 0 #5e9fd7; 419 | opacity: 0.95; 420 | outline: none; 421 | } 422 | .button-search:focus { 423 | background: #214b70; 424 | background: -webkit-gradient(linear, left bottom, left top, color-stop(0, #214b70), color-stop(1, #346691)); 425 | background: -ms-linear-gradient(bottom, #214b70, #346691); 426 | background: -moz-linear-gradient(center bottom, #214b70 0%, #346691 100%); 427 | background: -o-linear-gradient(#346691, #214b70); 428 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#346691', endColorstr='#214b70', GradientType=0); 429 | border: 1px solid #163754; 430 | box-shadow: inset 0px 1px 0 #3587cd; 431 | outline: none; 432 | } 433 | .footer { 434 | position: absolute; 435 | bottom: 0; 436 | width: 100%; 437 | } 438 | .panel-default, 439 | .panel { 440 | background: none; 441 | border: none; 442 | box-shadow: none; 443 | } 444 | .panel-default > .panel-heading { 445 | font-size: 18px; 446 | font-weight: 500; 447 | border: none; 448 | position: relative; 449 | background: none; 450 | } 451 | .panel-default > .panel-heading:after { 452 | content: ''; 453 | display: block; 454 | float: left; 455 | height: 1px; 456 | width: 100%; 457 | background: #000000; 458 | top: 0px; 459 | bottom: 0px; 460 | margin: auto; 461 | position: absolute; 462 | } 463 | .panel-default > .panel-heading .ico-panel { 464 | float: left; 465 | position: absolute; 466 | left: 10px; 467 | top: 0; 468 | background: #FFF; 469 | z-index: 3; 470 | } 471 | .panel-default > .panel-heading h3 { 472 | margin: 0; 473 | text-align: left; 474 | font-weight: 300; 475 | padding-left: 55px; 476 | display: inline; 477 | background: #FFF; 478 | position: relative; 479 | z-index: 2; 480 | padding-right: 15px; 481 | } 482 | .navbar-nav > li > a { 483 | border-top: 3px solid transparent; 484 | text-transform: uppercase; 485 | font-size: 14px; 486 | font-weight: normal; 487 | } 488 | .navbar-default .navbar-nav > .active > a, 489 | .navbar-default .navbar-nav > .active > a:hover, 490 | .navbar-default .navbar-nav > .active > a:focus { 491 | background: none; 492 | border-top: 3px solid #326c9e; 493 | color: #151515; 494 | } 495 | .btn-social { 496 | position: relative; 497 | padding-left: 44px; 498 | text-align: left; 499 | white-space: nowrap; 500 | overflow: hidden; 501 | text-overflow: ellipsis; 502 | font-weight: 300; 503 | background-repeat: no-repeat; 504 | background-position: 20px 10px; 505 | border-radius: 0; 506 | -webkit-transition: all 0.2s ease-out; 507 | -moz-transition: all 0.2s ease-out; 508 | -o-transition: all 0.2s ease-out; 509 | transition: all 0.2s ease-out; 510 | opacity: .75; 511 | } 512 | .btn-social:hover { 513 | opacity: 1; 514 | } 515 | .btn-social.btn-github { 516 | color: #fff; 517 | background-color: #2b2b2b; 518 | border-color: rgba(0, 0, 0, 0.2); 519 | text-decoration: none; 520 | background-image: url('images/github.png'); 521 | } 522 | .btn-social.btn-github:hover { 523 | background-color: #121212; 524 | } 525 | .btn-social.btn-twitter { 526 | color: #fff; 527 | background-color: #55acee; 528 | border-color: rgba(0, 0, 0, 0.2); 529 | background-image: url('images/twitter.png'); 530 | } 531 | .btn-social.btn-twitter:hover { 532 | background-color: #2795e9; 533 | } 534 | .btn-social.btn-lg { 535 | padding: 10px 16px; 536 | font-size: 18px; 537 | line-height: 1.33; 538 | padding-left: 61px; 539 | } 540 | -------------------------------------------------------------------------------- /src/Mouf/Packanalyst/Controllers/ClassAnalyzerController.php: -------------------------------------------------------------------------------- 1 | logger = $logger; 80 | $this->template = $template; 81 | $this->content = $content; 82 | $this->twig = $twig; 83 | $this->itemDao = $itemDao; 84 | $this->packageDao = $packageDao; 85 | } 86 | 87 | /** 88 | * @URL class 89 | * @Get 90 | * 91 | * @param string $q 92 | */ 93 | public function index($q) 94 | { 95 | 96 | // Remove front \ 97 | $q = ltrim($q, '\\'); 98 | 99 | $graphItems = $this->findItemsInheriting($q); 100 | 101 | if (count($graphItems) == self::LIMIT_INHERITS) { 102 | $inheritLimit = true; 103 | } else { 104 | $inheritLimit = false; 105 | } 106 | 107 | $rootNodesCollection = $this->itemDao->getItemsByName($q)->toArray(); 108 | // If there is no root node (for instance if the class is "Exception") 109 | if (count($rootNodesCollection) == 0) { 110 | 111 | // If this class has never been used, we might want to wonder if the class exists at all. 112 | if (count($graphItems) == 0) { 113 | // Let's go on a 404. 114 | header('HTTP/1.0 404 Not Found'); 115 | $this->content->addHtmlElement(new TwigTemplate($this->twig, 'src/views/classAnalyzer/404.twig', array('class' => $q))); 116 | $this->template->toHtml(); 117 | 118 | return; 119 | } 120 | 121 | $rootNodes = [[ 122 | 'name' => $q, 123 | ]]; 124 | } else { 125 | $rootNodes = []; 126 | foreach ($rootNodesCollection as $key => $item) { 127 | $item = (array) $item; 128 | $rootNodes[$key] = $item; 129 | $packageName = $item['packageName']; 130 | if (!isset($this->packagesCache[$packageName])) { 131 | $this->packagesCache[$packageName] = (array) $this->packageDao->getPackagesByName($packageName)->toArray()[0]; 132 | } 133 | $rootNodes[$key]['package'] = $this->packagesCache[$packageName]; 134 | } 135 | } 136 | $graph = new Graph($rootNodes, $graphItems); 137 | 138 | // Let's extract the PHPDoc from the latest version (dev version): 139 | $rootNode = null; 140 | foreach ($rootNodes as $rootNode) { 141 | if (isset($rootNode['packageVersion']) && strpos($rootNode['packageVersion'], '-dev') !== false) { 142 | break; 143 | } 144 | } 145 | if ($rootNode && isset($rootNode['phpDoc'])) { 146 | $docBlock = new MoufPhpDocComment($rootNode['phpDoc']); 147 | $md = $docBlock->getComment(); 148 | $description = MarkdownExtra::defaultTransform($md); 149 | 150 | // Let's purify HTML to avoid any attack: 151 | $config = \HTMLPurifier_Config::createDefault(); 152 | $purifier = new \HTMLPurifier($config); 153 | $description = $purifier->purify($description); 154 | } else { 155 | $description = ''; 156 | } 157 | if ($rootNode && isset($rootNode['type'])) { 158 | $type = $rootNode['type']; 159 | } else { 160 | $type = 'class'; 161 | } 162 | 163 | // Let's compute the pointer to the source. 164 | if (isset($rootNode['packageName'])) { 165 | // TODO: improve to get the link to the best package 166 | $package = $this->packageDao->get($rootNode['packageName'], $rootNode['packageVersion']); 167 | $sourceUrl = null; 168 | if (isset($package['sourceUrl']) && strpos($package['sourceUrl'], 'https://github.com') === 0) { 169 | if (strpos($package['sourceUrl'], '.git') === strlen($package['sourceUrl']) - 4) { 170 | if (isset($rootNode['fileName']) && $rootNode['fileName']) { 171 | $sourceUrl = substr($package['sourceUrl'], 0, strlen($package['sourceUrl']) - 4); 172 | $version = str_replace(['dev-', '.x-dev'], ['', ''], $package['packageVersion']); 173 | $sourceUrl .= '/blob/'.$version.$rootNode['fileName']; 174 | } 175 | } 176 | } 177 | } else { 178 | $sourceUrl = null; 179 | } 180 | 181 | // Now, let's find all the classes/interfaces we extend from (recursively...) 182 | $inheritNodes = $this->getNode($q); 183 | 184 | // Compute the revert depth of all elements. 185 | $inheritNodes->getRevertDepth(); 186 | 187 | // We put the graph of the extending classes INTO the revert graph of the classes we extend from. 188 | $inheritNodes->replaceNodeRenderingWith($graph); 189 | 190 | // Finally, let's get the list of classes/interfaces/traits/functions using this item 191 | $usedInItems = $this->itemDao->findItemsUsing($q, 1000); 192 | 193 | // Let's add the twig file to the template. 194 | $this->template->setTitle('Packanalyst | '.ucfirst($type).' '.$q); 195 | $this->template->getWebLibraryManager()->addLibrary(new WebLibrary([ROOT_URL.'src/views/classAnalyzer/classAnalyzer.js'])); 196 | 197 | \Mouf::getSearchBlock()->setSearch($q); 198 | 199 | $this->content->addHtmlElement(new TwigTemplate($this->twig, 'src/views/classAnalyzer/index.twig', 200 | array( 201 | 'class' => $q, 202 | //"graph"=>$graph, 203 | 'description' => $description, 204 | 'type' => $type, 205 | //"inheritNodes"=>$inheritNodes, 206 | 'inheritNodesHtml' => $inheritNodes->getHtmlRevert(), 207 | 'sourceUrl' => $sourceUrl, 208 | 'inheritLimit' => $inheritLimit, 209 | 'usedInItems' => $usedInItems, ))); 210 | $this->template->toHtml(); 211 | } 212 | 213 | /** 214 | * Finds a list of classes/interfaces inheriting the passed interface. 215 | * The items returned will contain a special "package" key pointing to the package array. 216 | * 217 | * @param string $className 218 | */ 219 | private function findItemsInheriting($className) 220 | { 221 | $graphItems = $this->itemDao->findItemsInheriting($className, self::LIMIT_INHERITS); 222 | 223 | $items = []; 224 | 225 | foreach ($graphItems as $item) { 226 | $item = (array) $item; 227 | $packageName = $item['packageName']; 228 | if (!isset($this->packagesCache[$packageName])) { 229 | $this->packagesCache[$packageName] = (array) $this->packageDao->getPackagesByName($packageName)->toArray()[0]; 230 | } 231 | $item['package'] = $this->packagesCache[$packageName]; 232 | $items[] = $item; 233 | } 234 | 235 | return $items; 236 | } 237 | 238 | private $inheritedNodes = array(); 239 | 240 | private function getNode($className, $antiLoopArray = array()) 241 | { 242 | if (isset($antiLoopArray[$className])) { 243 | return; 244 | } 245 | $antiLoopArray[$className] = true; 246 | 247 | if (isset($this->inheritedNodes[$className])) { 248 | return $this->inheritedNodes[$className]; 249 | } 250 | 251 | $nodes = $this->itemDao->getItemsByName($className)->toArray(); 252 | if ($nodes) { 253 | $mainNode = (array) $nodes[0]; 254 | $type = isset($mainNode['type']) ? $mainNode['type'] : null; 255 | } else { 256 | $type = null; 257 | } 258 | 259 | $htmlNode = new Node($className, $type); 260 | 261 | $inherits = array(); 262 | foreach ($nodes as $node) { 263 | $node = (array) $node; 264 | if (isset($node['packageName'])) { 265 | $packageName = $node['packageName']; 266 | if (!isset($this->packagesCache[$packageName])) { 267 | $this->packagesCache[$packageName] = (array) $this->packageDao->getPackagesByName($packageName)->toArray()[0]; 268 | } 269 | $htmlNode->registerPackage($node['packageName'], $node['packageVersion'], isset($this->packagesCache[$packageName]['downloads']) ? $this->packagesCache[$packageName]['downloads'] : null, isset($this->packagesCache[$packageName]['favers']) ? $this->packagesCache[$packageName]['favers'] : null); 270 | } 271 | if (isset($node['inherits'])) { 272 | $inherits = array_merge($inherits, (array) $node['inherits']); 273 | } 274 | } 275 | 276 | $this->inheritedNodes[$className] = $htmlNode; 277 | 278 | $inherits = array_keys(array_flip($inherits)); 279 | 280 | foreach ($inherits as $inherit) { 281 | $node = $this->getNode($inherit, $antiLoopArray); 282 | if ($node) { 283 | $htmlNode->addChild($node); 284 | } 285 | } 286 | 287 | return $htmlNode; 288 | } 289 | 290 | /** 291 | * Returns the list of classes/interfaces/traits that inherits/extends the class/interface/trait passed 292 | * in parameter. 293 | * Result is returned as a JSON result. 294 | * 295 | * @URL api/v1/inherits 296 | * @Get 297 | * 298 | * @param string $q 299 | */ 300 | public function inherits($q) 301 | { 302 | 303 | // Remove front \ 304 | $q = ltrim($q, '\\'); 305 | 306 | $graphItems = $this->findItemsInheriting($q); 307 | 308 | $rootNodesCollection = $this->itemDao->getItemsByName($q)->toArray(); 309 | // If there is no root node (for instance if the class is "Exception") 310 | if (count($rootNodesCollection) == 0) { 311 | 312 | // If this class has never been used, we might want to wonder if the class exists at all. 313 | if (count($graphItems) == 0) { 314 | // Let's go on a 404. 315 | 316 | return new JsonResponse(['status' => 'error', 'message' => "Item '$q' does not exist."], 404); 317 | } 318 | 319 | $rootNodes = [[ 320 | 'name' => $q, 321 | ]]; 322 | } else { 323 | $rootNodes = []; 324 | foreach ($rootNodesCollection as $key => $item) { 325 | $rootNodes[$key] = $item; 326 | $packageName = $item['packageName']; 327 | if (!isset($this->packagesCache[$packageName])) { 328 | $this->packagesCache[$packageName] = $this->packageDao->getPackagesByName($packageName)->getNext(); 329 | } 330 | $rootNodes[$key]['package'] = $this->packagesCache[$packageName]; 331 | } 332 | } 333 | $graph = new Graph($rootNodes, $graphItems); 334 | 335 | // Let's extract the PHPDoc from the latest version (dev version): 336 | $rootNode = null; 337 | foreach ($rootNodes as $rootNode) { 338 | if (isset($rootNode['packageVersion']) && strpos($rootNode['packageVersion'], '-dev') !== false) { 339 | break; 340 | } 341 | } 342 | if ($rootNode && isset($rootNode['phpDoc'])) { 343 | $docBlock = new MoufPhpDocComment($rootNode['phpDoc']); 344 | $md = $docBlock->getComment(); 345 | $description = MarkdownExtra::defaultTransform($md); 346 | 347 | // Let's purify HTML to avoid any attack: 348 | $config = \HTMLPurifier_Config::createDefault(); 349 | $purifier = new \HTMLPurifier($config); 350 | $description = $purifier->purify($description); 351 | } else { 352 | $description = ''; 353 | } 354 | if ($rootNode && isset($rootNode['type'])) { 355 | $type = $rootNode['type']; 356 | } else { 357 | $type = 'class'; 358 | } 359 | 360 | // Let's compute the pointer to the source. 361 | if (isset($rootNode['packageName'])) { 362 | // TODO: improve to get the link to the best package 363 | $package = $this->packageDao->get($rootNode['packageName'], $rootNode['packageVersion']); 364 | $sourceUrl = null; 365 | if (isset($package['sourceUrl']) && strpos($package['sourceUrl'], 'https://github.com') === 0) { 366 | if (strpos($package['sourceUrl'], '.git') === strlen($package['sourceUrl']) - 4) { 367 | if (isset($rootNode['fileName']) && $rootNode['fileName']) { 368 | $sourceUrl = substr($package['sourceUrl'], 0, strlen($package['sourceUrl']) - 4); 369 | $version = str_replace(['dev-', '.x-dev'], ['', ''], $package['packageVersion']); 370 | $sourceUrl .= '/blob/'.$version.$rootNode['fileName']; 371 | } 372 | } 373 | } 374 | } else { 375 | $sourceUrl = null; 376 | } 377 | 378 | // Now, let's find all the classes/interfaces we extend from (recursively...) 379 | $inheritNodes = $this->getNode($q); 380 | 381 | // Compute the revert depth of all elements. 382 | $inheritNodes->getRevertDepth(); 383 | 384 | // We put the graph of the extending classes INTO the revert graph of the classes we extend from. 385 | $inheritNodes->replaceNodeRenderingWith($graph); 386 | 387 | // Finally, let's get the list of classes/interfaces/traits/functions using this item 388 | $usedInItems = $this->itemDao->findItemsUsing($q, 1000); 389 | 390 | // Let's add the twig file to the template. 391 | $this->template->setTitle('Packanalyst | '.ucfirst($type).' '.$q); 392 | $this->template->getWebLibraryManager()->addLibrary(new WebLibrary([ROOT_URL.'src/views/classAnalyzer/classAnalyzer.js'])); 393 | 394 | \Mouf::getSearchBlock()->setSearch($q); 395 | 396 | $this->content->addHtmlElement(new TwigTemplate($this->twig, 'src/views/classAnalyzer/index.twig', 397 | array( 398 | 'class' => $q, 399 | //"graph"=>$graph, 400 | 'description' => $description, 401 | 'type' => $type, 402 | //"inheritNodes"=>$inheritNodes, 403 | 'inheritNodesHtml' => $inheritNodes->getHtmlRevert(), 404 | 'sourceUrl' => $sourceUrl, 405 | 'usedInItems' => $usedInItems, ))); 406 | $this->template->toHtml(); 407 | } 408 | } 409 | -------------------------------------------------------------------------------- /components/typeaheadjs/typeaheadjs-built.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * typeahead.js 0.10.5 3 | * https://github.com/twitter/typeahead.js 4 | * Copyright 2013-2014 Twitter, Inc. and other contributors; Licensed MIT 5 | */ 6 | 7 | !function(a){var b=function(){"use strict";return{isMsie:function(){return/(msie|trident)/i.test(navigator.userAgent)?navigator.userAgent.match(/(msie |rv:)(\d+(.\d+)?)/i)[2]:!1},isBlankString:function(a){return!a||/^\s*$/.test(a)},escapeRegExChars:function(a){return a.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g,"\\$&")},isString:function(a){return"string"==typeof a},isNumber:function(a){return"number"==typeof a},isArray:a.isArray,isFunction:a.isFunction,isObject:a.isPlainObject,isUndefined:function(a){return"undefined"==typeof a},toStr:function(a){return b.isUndefined(a)||null===a?"":a+""},bind:a.proxy,each:function(b,c){function d(a,b){return c(b,a)}a.each(b,d)},map:a.map,filter:a.grep,every:function(b,c){var d=!0;return b?(a.each(b,function(a,e){return(d=c.call(null,e,a,b))?void 0:!1}),!!d):d},some:function(b,c){var d=!1;return b?(a.each(b,function(a,e){return(d=c.call(null,e,a,b))?!1:void 0}),!!d):d},mixin:a.extend,getUniqueId:function(){var a=0;return function(){return a++}}(),templatify:function(b){function c(){return String(b)}return a.isFunction(b)?b:c},defer:function(a){setTimeout(a,0)},debounce:function(a,b,c){var d,e;return function(){var f,g,h=this,i=arguments;return f=function(){d=null,c||(e=a.apply(h,i))},g=c&&!d,clearTimeout(d),d=setTimeout(f,b),g&&(e=a.apply(h,i)),e}},throttle:function(a,b){var c,d,e,f,g,h;return g=0,h=function(){g=new Date,e=null,f=a.apply(c,d)},function(){var i=new Date,j=b-(i-g);return c=this,d=arguments,0>=j?(clearTimeout(e),e=null,g=i,f=a.apply(c,d)):e||(e=setTimeout(h,j)),f}},noop:function(){}}}(),c="0.10.5",d=function(){"use strict";function a(a){return a=b.toStr(a),a?a.split(/\s+/):[]}function c(a){return a=b.toStr(a),a?a.split(/\W+/):[]}function d(a){return function(){var c=[].slice.call(arguments,0);return function(d){var e=[];return b.each(c,function(c){e=e.concat(a(b.toStr(d[c])))}),e}}}return{nonword:c,whitespace:a,obj:{nonword:d(c),whitespace:d(a)}}}(),e=function(){"use strict";function c(c){this.maxSize=b.isNumber(c)?c:100,this.reset(),this.maxSize<=0&&(this.set=this.get=a.noop)}function d(){this.head=this.tail=null}function e(a,b){this.key=a,this.val=b,this.prev=this.next=null}return b.mixin(c.prototype,{set:function(a,b){var c,d=this.list.tail;this.size>=this.maxSize&&(this.list.remove(d),delete this.hash[d.key]),(c=this.hash[a])?(c.val=b,this.list.moveToFront(c)):(c=new e(a,b),this.list.add(c),this.hash[a]=c,this.size++)},get:function(a){var b=this.hash[a];return b?(this.list.moveToFront(b),b.val):void 0},reset:function(){this.size=0,this.hash={},this.list=new d}}),b.mixin(d.prototype,{add:function(a){this.head&&(a.next=this.head,this.head.prev=a),this.head=a,this.tail=this.tail||a},remove:function(a){a.prev?a.prev.next=a.next:this.head=a.next,a.next?a.next.prev=a.prev:this.tail=a.prev},moveToFront:function(a){this.remove(a),this.add(a)}}),c}(),f=function(){"use strict";function a(a){this.prefix=["__",a,"__"].join(""),this.ttlKey="__ttl__",this.keyMatcher=new RegExp("^"+b.escapeRegExChars(this.prefix))}function c(){return(new Date).getTime()}function d(a){return JSON.stringify(b.isUndefined(a)?null:a)}function e(a){return JSON.parse(a)}var f,g;try{f=window.localStorage,f.setItem("~~~","!"),f.removeItem("~~~")}catch(h){f=null}return g=f&&window.JSON?{_prefix:function(a){return this.prefix+a},_ttlKey:function(a){return this._prefix(a)+this.ttlKey},get:function(a){return this.isExpired(a)&&this.remove(a),e(f.getItem(this._prefix(a)))},set:function(a,e,g){return b.isNumber(g)?f.setItem(this._ttlKey(a),d(c()+g)):f.removeItem(this._ttlKey(a)),f.setItem(this._prefix(a),d(e))},remove:function(a){return f.removeItem(this._ttlKey(a)),f.removeItem(this._prefix(a)),this},clear:function(){var a,b,c=[],d=f.length;for(a=0;d>a;a++)(b=f.key(a)).match(this.keyMatcher)&&c.push(b.replace(this.keyMatcher,""));for(a=c.length;a--;)this.remove(c[a]);return this},isExpired:function(a){var d=e(f.getItem(this._ttlKey(a)));return b.isNumber(d)&&c()>d?!0:!1}}:{get:b.noop,set:b.noop,remove:b.noop,clear:b.noop,isExpired:b.noop},b.mixin(a.prototype,g),a}(),g=function(){"use strict";function c(b){b=b||{},this.cancelled=!1,this.lastUrl=null,this._send=b.transport?d(b.transport):a.ajax,this._get=b.rateLimiter?b.rateLimiter(this._get):this._get,this._cache=b.cache===!1?new e(0):i}function d(c){return function(d,e){function f(a){b.defer(function(){h.resolve(a)})}function g(a){b.defer(function(){h.reject(a)})}var h=a.Deferred();return c(d,e,f,g),h}}var f=0,g={},h=6,i=new e(10);return c.setMaxPendingRequests=function(a){h=a},c.resetCache=function(){i.reset()},b.mixin(c.prototype,{_get:function(a,b,c){function d(b){c&&c(null,b),k._cache.set(a,b)}function e(){c&&c(!0)}function i(){f--,delete g[a],k.onDeckRequestArgs&&(k._get.apply(k,k.onDeckRequestArgs),k.onDeckRequestArgs=null)}var j,k=this;this.cancelled||a!==this.lastUrl||((j=g[a])?j.done(d).fail(e):h>f?(f++,g[a]=this._send(a,b).done(d).fail(e).always(i)):this.onDeckRequestArgs=[].slice.call(arguments,0))},get:function(a,c,d){var e;return b.isFunction(c)&&(d=c,c={}),this.cancelled=!1,this.lastUrl=a,(e=this._cache.get(a))?b.defer(function(){d&&d(null,e)}):this._get(a,c,d),!!e},cancel:function(){this.cancelled=!0}}),c}(),h=function(){"use strict";function c(b){b=b||{},b.datumTokenizer&&b.queryTokenizer||a.error("datumTokenizer and queryTokenizer are both required"),this.datumTokenizer=b.datumTokenizer,this.queryTokenizer=b.queryTokenizer,this.reset()}function d(a){return a=b.filter(a,function(a){return!!a}),a=b.map(a,function(a){return a.toLowerCase()})}function e(){return{ids:[],children:{}}}function f(a){for(var b={},c=[],d=0,e=a.length;e>d;d++)b[a[d]]||(b[a[d]]=!0,c.push(a[d]));return c}function g(a,b){function c(a,b){return a-b}var d=0,e=0,f=[];a=a.sort(c),b=b.sort(c);for(var g=a.length,h=b.length;g>d&&h>e;)a[d]b[e]?e++:(f.push(a[d]),d++,e++);return f}return b.mixin(c.prototype,{bootstrap:function(a){this.datums=a.datums,this.trie=a.trie},add:function(a){var c=this;a=b.isArray(a)?a:[a],b.each(a,function(a){var f,g;f=c.datums.push(a)-1,g=d(c.datumTokenizer(a)),b.each(g,function(a){var b,d,g;for(b=c.trie,d=a.split("");g=d.shift();)b=b.children[g]||(b.children[g]=e()),b.ids.push(f)})})},get:function(a){var c,e,h=this;return c=d(this.queryTokenizer(a)),b.each(c,function(a){var b,c,d,f;if(e&&0===e.length)return!1;for(b=h.trie,c=a.split("");b&&(d=c.shift());)b=b.children[d];return b&&0===c.length?(f=b.ids.slice(0),void(e=e?g(e,f):f)):(e=[],!1)}),e?b.map(f(e),function(a){return h.datums[a]}):[]},reset:function(){this.datums=[],this.trie=e()},serialize:function(){return{datums:this.datums,trie:this.trie}}}),c}(),i=function(){"use strict";function d(a){return a.local||null}function e(d){var e,f;return f={url:null,thumbprint:"",ttl:864e5,filter:null,ajax:{}},(e=d.prefetch||null)&&(e=b.isString(e)?{url:e}:e,e=b.mixin(f,e),e.thumbprint=c+e.thumbprint,e.ajax.type=e.ajax.type||"GET",e.ajax.dataType=e.ajax.dataType||"json",!e.url&&a.error("prefetch requires url to be set")),e}function f(c){function d(a){return function(c){return b.debounce(c,a)}}function e(a){return function(c){return b.throttle(c,a)}}var f,g;return g={url:null,cache:!0,wildcard:"%QUERY",replace:null,rateLimitBy:"debounce",rateLimitWait:300,send:null,filter:null,ajax:{}},(f=c.remote||null)&&(f=b.isString(f)?{url:f}:f,f=b.mixin(g,f),f.rateLimiter=/^throttle$/i.test(f.rateLimitBy)?e(f.rateLimitWait):d(f.rateLimitWait),f.ajax.type=f.ajax.type||"GET",f.ajax.dataType=f.ajax.dataType||"json",delete f.rateLimitBy,delete f.rateLimitWait,!f.url&&a.error("remote requires url to be set")),f}return{local:d,prefetch:e,remote:f}}();!function(c){"use strict";function e(b){b&&(b.local||b.prefetch||b.remote)||a.error("one of local, prefetch, or remote is required"),this.limit=b.limit||5,this.sorter=j(b.sorter),this.dupDetector=b.dupDetector||k,this.local=i.local(b),this.prefetch=i.prefetch(b),this.remote=i.remote(b),this.cacheKey=this.prefetch?this.prefetch.cacheKey||this.prefetch.url:null,this.index=new h({datumTokenizer:b.datumTokenizer,queryTokenizer:b.queryTokenizer}),this.storage=this.cacheKey?new f(this.cacheKey):null}function j(a){function c(b){return b.sort(a)}function d(a){return a}return b.isFunction(a)?c:d}function k(){return!1}var l,m;return l=c.Bloodhound,m={data:"data",protocol:"protocol",thumbprint:"thumbprint"},c.Bloodhound=e,e.noConflict=function(){return c.Bloodhound=l,e},e.tokenizers=d,b.mixin(e.prototype,{_loadPrefetch:function(b){function c(a){f.clear(),f.add(b.filter?b.filter(a):a),f._saveToStorage(f.index.serialize(),b.thumbprint,b.ttl)}var d,e,f=this;return(d=this._readFromStorage(b.thumbprint))?(this.index.bootstrap(d),e=a.Deferred().resolve()):e=a.ajax(b.url,b.ajax).done(c),e},_getFromRemote:function(a,b){function c(a,c){b(a?[]:f.remote.filter?f.remote.filter(c):c)}var d,e,f=this;if(this.transport)return a=a||"",e=encodeURIComponent(a),d=this.remote.replace?this.remote.replace(this.remote.url,a):this.remote.url.replace(this.remote.wildcard,e),this.transport.get(d,this.remote.ajax,c)},_cancelLastRemoteRequest:function(){this.transport&&this.transport.cancel()},_saveToStorage:function(a,b,c){this.storage&&(this.storage.set(m.data,a,c),this.storage.set(m.protocol,location.protocol,c),this.storage.set(m.thumbprint,b,c))},_readFromStorage:function(a){var b,c={};return this.storage&&(c.data=this.storage.get(m.data),c.protocol=this.storage.get(m.protocol),c.thumbprint=this.storage.get(m.thumbprint)),b=c.thumbprint!==a||c.protocol!==location.protocol,c.data&&!b?c.data:null},_initialize:function(){function c(){e.add(b.isFunction(f)?f():f)}var d,e=this,f=this.local;return d=this.prefetch?this._loadPrefetch(this.prefetch):a.Deferred().resolve(),f&&d.done(c),this.transport=this.remote?new g(this.remote):null,this.initPromise=d.promise()},initialize:function(a){return!this.initPromise||a?this._initialize():this.initPromise},add:function(a){this.index.add(a)},get:function(a,c){function d(a){var d=f.slice(0);b.each(a,function(a){var c;return c=b.some(d,function(b){return e.dupDetector(a,b)}),!c&&d.push(a),d.length0||!this.transport)&&c&&c(f)},clear:function(){this.index.reset()},clearPrefetchCache:function(){this.storage&&this.storage.clear()},clearRemoteCache:function(){this.transport&&g.resetCache()},ttAdapter:function(){return b.bind(this.get,this)}}),e}(this);var j=function(){return{wrapper:'',dropdown:'',dataset:'
    ',suggestions:'',suggestion:'
    '}}(),k=function(){"use strict";var a={wrapper:{position:"relative",display:"inline-block"},hint:{position:"absolute",top:"0",left:"0",borderColor:"transparent",boxShadow:"none",opacity:"1"},input:{position:"relative",verticalAlign:"top",backgroundColor:"transparent"},inputWithNoHint:{position:"relative",verticalAlign:"top"},dropdown:{position:"absolute",top:"100%",left:"0",zIndex:"100",display:"none"},suggestions:{display:"block"},suggestion:{whiteSpace:"nowrap",cursor:"pointer"},suggestionChild:{whiteSpace:"normal"},ltr:{left:"0",right:"auto"},rtl:{left:"auto",right:" 0"}};return b.isMsie()&&b.mixin(a.input,{backgroundImage:"url(data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7)"}),b.isMsie()&&b.isMsie()<=7&&b.mixin(a.input,{marginTop:"-1px"}),a}(),l=function(){"use strict";function c(b){b&&b.el||a.error("EventBus initialized without el"),this.$el=a(b.el)}var d="typeahead:";return b.mixin(c.prototype,{trigger:function(a){var b=[].slice.call(arguments,1);this.$el.trigger(d+a,b)}}),c}(),m=function(){"use strict";function a(a,b,c,d){var e;if(!c)return this;for(b=b.split(i),c=d?h(c,d):c,this._callbacks=this._callbacks||{};e=b.shift();)this._callbacks[e]=this._callbacks[e]||{sync:[],async:[]},this._callbacks[e][a].push(c);return this}function b(b,c,d){return a.call(this,"async",b,c,d)}function c(b,c,d){return a.call(this,"sync",b,c,d)}function d(a){var b;if(!this._callbacks)return this;for(a=a.split(i);b=a.shift();)delete this._callbacks[b];return this}function e(a){var b,c,d,e,g;if(!this._callbacks)return this;for(a=a.split(i),d=[].slice.call(arguments,1);(b=a.shift())&&(c=this._callbacks[b]);)e=f(c.sync,this,[b].concat(d)),g=f(c.async,this,[b].concat(d)),e()&&j(g);return this}function f(a,b,c){function d(){for(var d,e=0,f=a.length;!d&&f>e;e+=1)d=a[e].apply(b,c)===!1;return!d}return d}function g(){var a;return a=window.setImmediate?function(a){setImmediate(function(){a()})}:function(a){setTimeout(function(){a()},0)}}function h(a,b){return a.bind?a.bind(b):function(){a.apply(b,[].slice.call(arguments,0))}}var i=/\s+/,j=g();return{onSync:c,onAsync:b,off:d,trigger:e}}(),n=function(a){"use strict";function c(a,c,d){for(var e,f=[],g=0,h=a.length;h>g;g++)f.push(b.escapeRegExChars(a[g]));return e=d?"\\b("+f.join("|")+")\\b":"("+f.join("|")+")",c?new RegExp(e):new RegExp(e,"i")}var d={node:null,pattern:null,tagName:"strong",className:null,wordsOnly:!1,caseSensitive:!1};return function(e){function f(b){var c,d,f;return(c=h.exec(b.data))&&(f=a.createElement(e.tagName),e.className&&(f.className=e.className),d=b.splitText(c.index),d.splitText(c[0].length),f.appendChild(d.cloneNode(!0)),b.parentNode.replaceChild(f,d)),!!c}function g(a,b){for(var c,d=3,e=0;e