api de gestion de ticket, basé sur php-crud-api. Le but est de décorrélé les outils de gestion des données, afin
Você não pode selecionar mais de 25 tópicos Os tópicos devem começar com uma letra ou um número, podem incluir traços ('-') e podem ter até 35 caracteres.

api.php 244KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026202720282029203020312032203320342035203620372038203920402041204220432044204520462047204820492050205120522053205420552056205720582059206020612062206320642065206620672068206920702071207220732074207520762077207820792080208120822083208420852086208720882089209020912092209320942095209620972098209921002101210221032104210521062107210821092110211121122113211421152116211721182119212021212122212321242125212621272128212921302131213221332134213521362137213821392140214121422143214421452146214721482149215021512152215321542155215621572158215921602161216221632164216521662167216821692170217121722173217421752176217721782179218021812182218321842185218621872188218921902191219221932194219521962197219821992200220122022203220422052206220722082209221022112212221322142215221622172218221922202221222222232224222522262227222822292230223122322233223422352236223722382239224022412242224322442245224622472248224922502251225222532254225522562257225822592260226122622263226422652266226722682269227022712272227322742275227622772278227922802281228222832284228522862287228822892290229122922293229422952296229722982299230023012302230323042305230623072308230923102311231223132314231523162317231823192320232123222323232423252326232723282329233023312332233323342335233623372338233923402341234223432344234523462347234823492350235123522353235423552356235723582359236023612362236323642365236623672368236923702371237223732374237523762377237823792380238123822383238423852386238723882389239023912392239323942395239623972398239924002401240224032404240524062407240824092410241124122413241424152416241724182419242024212422242324242425242624272428242924302431243224332434243524362437243824392440244124422443244424452446244724482449245024512452245324542455245624572458245924602461246224632464246524662467246824692470247124722473247424752476247724782479248024812482248324842485248624872488248924902491249224932494249524962497249824992500250125022503250425052506250725082509251025112512251325142515251625172518251925202521252225232524252525262527252825292530253125322533253425352536253725382539254025412542254325442545254625472548254925502551255225532554255525562557255825592560256125622563256425652566256725682569257025712572257325742575257625772578257925802581258225832584258525862587258825892590259125922593259425952596259725982599260026012602260326042605260626072608260926102611261226132614261526162617261826192620262126222623262426252626262726282629263026312632263326342635263626372638263926402641264226432644264526462647264826492650265126522653265426552656265726582659266026612662266326642665266626672668266926702671267226732674267526762677267826792680268126822683268426852686268726882689269026912692269326942695269626972698269927002701270227032704270527062707270827092710271127122713271427152716271727182719272027212722272327242725272627272728272927302731273227332734273527362737273827392740274127422743274427452746274727482749275027512752275327542755275627572758275927602761276227632764276527662767276827692770277127722773277427752776277727782779278027812782278327842785278627872788278927902791279227932794279527962797279827992800280128022803280428052806280728082809281028112812281328142815281628172818281928202821282228232824282528262827282828292830283128322833283428352836283728382839284028412842284328442845284628472848284928502851285228532854285528562857285828592860286128622863286428652866286728682869287028712872287328742875287628772878287928802881288228832884288528862887288828892890289128922893289428952896289728982899290029012902290329042905290629072908290929102911291229132914291529162917291829192920292129222923292429252926292729282929293029312932293329342935293629372938293929402941294229432944294529462947294829492950295129522953295429552956295729582959296029612962296329642965296629672968296929702971297229732974297529762977297829792980298129822983298429852986298729882989299029912992299329942995299629972998299930003001300230033004300530063007300830093010301130123013301430153016301730183019302030213022302330243025302630273028302930303031303230333034303530363037303830393040304130423043304430453046304730483049305030513052305330543055305630573058305930603061306230633064306530663067306830693070307130723073307430753076307730783079308030813082308330843085308630873088308930903091309230933094309530963097309830993100310131023103310431053106310731083109311031113112311331143115311631173118311931203121312231233124312531263127312831293130313131323133313431353136313731383139314031413142314331443145314631473148314931503151315231533154315531563157315831593160316131623163316431653166316731683169317031713172317331743175317631773178317931803181318231833184318531863187318831893190319131923193319431953196319731983199320032013202320332043205320632073208320932103211321232133214321532163217321832193220322132223223322432253226322732283229323032313232323332343235323632373238323932403241324232433244324532463247324832493250325132523253325432553256325732583259326032613262326332643265326632673268326932703271327232733274327532763277327832793280328132823283328432853286328732883289329032913292329332943295329632973298329933003301330233033304330533063307330833093310331133123313331433153316331733183319332033213322332333243325332633273328332933303331333233333334333533363337333833393340334133423343334433453346334733483349335033513352335333543355335633573358335933603361336233633364336533663367336833693370337133723373337433753376337733783379338033813382338333843385338633873388338933903391339233933394339533963397339833993400340134023403340434053406340734083409341034113412341334143415341634173418341934203421342234233424342534263427342834293430343134323433343434353436343734383439344034413442344334443445344634473448344934503451345234533454345534563457345834593460346134623463346434653466346734683469347034713472347334743475347634773478347934803481348234833484348534863487348834893490349134923493349434953496349734983499350035013502350335043505350635073508350935103511351235133514351535163517351835193520352135223523352435253526352735283529353035313532353335343535353635373538353935403541354235433544354535463547354835493550355135523553355435553556355735583559356035613562356335643565356635673568356935703571357235733574357535763577357835793580358135823583358435853586358735883589359035913592359335943595359635973598359936003601360236033604360536063607360836093610361136123613361436153616361736183619362036213622362336243625362636273628362936303631363236333634363536363637363836393640364136423643364436453646364736483649365036513652365336543655365636573658365936603661366236633664366536663667366836693670367136723673367436753676367736783679368036813682368336843685368636873688368936903691369236933694369536963697369836993700370137023703370437053706370737083709371037113712371337143715371637173718371937203721372237233724372537263727372837293730373137323733373437353736373737383739374037413742374337443745374637473748374937503751375237533754375537563757375837593760376137623763376437653766376737683769377037713772377337743775377637773778377937803781378237833784378537863787378837893790379137923793379437953796379737983799380038013802380338043805380638073808380938103811381238133814381538163817381838193820382138223823382438253826382738283829383038313832383338343835383638373838383938403841384238433844384538463847384838493850385138523853385438553856385738583859386038613862386338643865386638673868386938703871387238733874387538763877387838793880388138823883388438853886388738883889389038913892389338943895389638973898389939003901390239033904390539063907390839093910391139123913391439153916391739183919392039213922392339243925392639273928392939303931393239333934393539363937393839393940394139423943394439453946394739483949395039513952395339543955395639573958395939603961396239633964396539663967396839693970397139723973397439753976397739783979398039813982398339843985398639873988398939903991399239933994399539963997399839994000400140024003400440054006400740084009401040114012401340144015401640174018401940204021402240234024402540264027402840294030403140324033403440354036403740384039404040414042404340444045404640474048404940504051405240534054405540564057405840594060406140624063406440654066406740684069407040714072407340744075407640774078407940804081408240834084408540864087408840894090409140924093409440954096409740984099410041014102410341044105410641074108410941104111411241134114411541164117411841194120412141224123412441254126412741284129413041314132413341344135413641374138413941404141414241434144414541464147414841494150415141524153415441554156415741584159416041614162416341644165416641674168416941704171417241734174417541764177417841794180418141824183418441854186418741884189419041914192419341944195419641974198419942004201420242034204420542064207420842094210421142124213421442154216421742184219422042214222422342244225422642274228422942304231423242334234423542364237423842394240424142424243424442454246424742484249425042514252425342544255425642574258425942604261426242634264426542664267426842694270427142724273427442754276427742784279428042814282428342844285428642874288428942904291429242934294429542964297429842994300430143024303430443054306430743084309431043114312431343144315431643174318431943204321432243234324432543264327432843294330433143324333433443354336433743384339434043414342434343444345434643474348434943504351435243534354435543564357435843594360436143624363436443654366436743684369437043714372437343744375437643774378437943804381438243834384438543864387438843894390439143924393439443954396439743984399440044014402440344044405440644074408440944104411441244134414441544164417441844194420442144224423442444254426442744284429443044314432443344344435443644374438443944404441444244434444444544464447444844494450445144524453445444554456445744584459446044614462446344644465446644674468446944704471447244734474447544764477447844794480448144824483448444854486448744884489449044914492449344944495449644974498449945004501450245034504450545064507450845094510451145124513451445154516451745184519452045214522452345244525452645274528452945304531453245334534453545364537453845394540454145424543454445454546454745484549455045514552455345544555455645574558455945604561456245634564456545664567456845694570457145724573457445754576457745784579458045814582458345844585458645874588458945904591459245934594459545964597459845994600460146024603460446054606460746084609461046114612461346144615461646174618461946204621462246234624462546264627462846294630463146324633463446354636463746384639464046414642464346444645464646474648464946504651465246534654465546564657465846594660466146624663466446654666466746684669467046714672467346744675467646774678467946804681468246834684468546864687468846894690469146924693469446954696469746984699470047014702470347044705470647074708470947104711471247134714471547164717471847194720472147224723472447254726472747284729473047314732473347344735473647374738473947404741474247434744474547464747474847494750475147524753475447554756475747584759476047614762476347644765476647674768476947704771477247734774477547764777477847794780478147824783478447854786478747884789479047914792479347944795479647974798479948004801480248034804480548064807480848094810481148124813481448154816481748184819482048214822482348244825482648274828482948304831483248334834483548364837483848394840484148424843484448454846484748484849485048514852485348544855485648574858485948604861486248634864486548664867486848694870487148724873487448754876487748784879488048814882488348844885488648874888488948904891489248934894489548964897489848994900490149024903490449054906490749084909491049114912491349144915491649174918491949204921492249234924492549264927492849294930493149324933493449354936493749384939494049414942494349444945494649474948494949504951495249534954495549564957495849594960496149624963496449654966496749684969497049714972497349744975497649774978497949804981498249834984498549864987498849894990499149924993499449954996499749984999500050015002500350045005500650075008500950105011501250135014501550165017501850195020502150225023502450255026502750285029503050315032503350345035503650375038503950405041504250435044504550465047504850495050505150525053505450555056505750585059506050615062506350645065506650675068506950705071507250735074507550765077507850795080508150825083508450855086508750885089509050915092509350945095509650975098509951005101510251035104510551065107510851095110511151125113511451155116511751185119512051215122512351245125512651275128512951305131513251335134513551365137513851395140514151425143514451455146514751485149515051515152515351545155515651575158515951605161516251635164516551665167516851695170517151725173517451755176517751785179518051815182518351845185518651875188518951905191519251935194519551965197519851995200520152025203520452055206520752085209521052115212521352145215521652175218521952205221522252235224522552265227522852295230523152325233523452355236523752385239524052415242524352445245524652475248524952505251525252535254525552565257525852595260526152625263526452655266526752685269527052715272527352745275527652775278527952805281528252835284528552865287528852895290529152925293529452955296529752985299530053015302530353045305530653075308530953105311531253135314531553165317531853195320532153225323532453255326532753285329533053315332533353345335533653375338533953405341534253435344534553465347534853495350535153525353535453555356535753585359536053615362536353645365536653675368536953705371537253735374537553765377537853795380538153825383538453855386538753885389539053915392539353945395539653975398539954005401540254035404540554065407540854095410541154125413541454155416541754185419542054215422542354245425542654275428542954305431543254335434543554365437543854395440544154425443544454455446544754485449545054515452545354545455545654575458545954605461546254635464546554665467546854695470547154725473547454755476547754785479548054815482548354845485548654875488548954905491549254935494549554965497549854995500550155025503550455055506550755085509551055115512551355145515551655175518551955205521552255235524552555265527552855295530553155325533553455355536553755385539554055415542554355445545554655475548554955505551555255535554555555565557555855595560556155625563556455655566556755685569557055715572557355745575557655775578557955805581558255835584558555865587558855895590559155925593559455955596559755985599560056015602560356045605560656075608560956105611561256135614561556165617561856195620562156225623562456255626562756285629563056315632563356345635563656375638563956405641564256435644564556465647564856495650565156525653565456555656565756585659566056615662566356645665566656675668566956705671567256735674567556765677567856795680568156825683568456855686568756885689569056915692569356945695569656975698569957005701570257035704570557065707570857095710571157125713571457155716571757185719572057215722572357245725572657275728572957305731573257335734573557365737573857395740574157425743574457455746574757485749575057515752575357545755575657575758575957605761576257635764576557665767576857695770577157725773577457755776577757785779578057815782578357845785578657875788578957905791579257935794579557965797579857995800580158025803580458055806580758085809581058115812581358145815581658175818581958205821582258235824582558265827582858295830583158325833583458355836583758385839584058415842584358445845584658475848584958505851585258535854585558565857585858595860586158625863586458655866586758685869587058715872587358745875587658775878587958805881588258835884588558865887588858895890589158925893589458955896589758985899590059015902590359045905590659075908590959105911591259135914591559165917591859195920592159225923592459255926592759285929593059315932593359345935593659375938593959405941594259435944594559465947594859495950595159525953595459555956595759585959596059615962596359645965596659675968596959705971597259735974597559765977597859795980598159825983598459855986598759885989599059915992599359945995599659975998599960006001600260036004600560066007600860096010601160126013601460156016601760186019602060216022602360246025602660276028602960306031603260336034603560366037603860396040604160426043604460456046604760486049605060516052605360546055605660576058605960606061606260636064606560666067606860696070607160726073607460756076607760786079608060816082608360846085608660876088608960906091609260936094609560966097609860996100610161026103610461056106610761086109611061116112611361146115611661176118611961206121612261236124612561266127612861296130613161326133613461356136613761386139614061416142614361446145614661476148614961506151615261536154615561566157615861596160616161626163616461656166616761686169617061716172617361746175617661776178617961806181618261836184618561866187618861896190619161926193619461956196619761986199620062016202620362046205620662076208620962106211621262136214621562166217621862196220622162226223622462256226622762286229623062316232623362346235623662376238623962406241624262436244624562466247624862496250625162526253625462556256625762586259626062616262626362646265626662676268626962706271627262736274627562766277627862796280628162826283628462856286628762886289629062916292629362946295629662976298629963006301630263036304630563066307630863096310631163126313631463156316631763186319632063216322632363246325632663276328632963306331633263336334633563366337633863396340634163426343634463456346634763486349635063516352635363546355635663576358635963606361636263636364636563666367636863696370637163726373637463756376637763786379638063816382638363846385638663876388638963906391639263936394639563966397639863996400640164026403640464056406640764086409641064116412641364146415641664176418641964206421642264236424642564266427642864296430643164326433643464356436643764386439644064416442644364446445644664476448644964506451645264536454645564566457645864596460646164626463646464656466646764686469647064716472647364746475647664776478647964806481648264836484648564866487648864896490649164926493649464956496649764986499650065016502650365046505650665076508650965106511651265136514651565166517651865196520652165226523652465256526652765286529653065316532653365346535653665376538653965406541654265436544654565466547654865496550655165526553655465556556655765586559656065616562656365646565656665676568656965706571657265736574657565766577657865796580658165826583658465856586658765886589659065916592659365946595659665976598659966006601660266036604660566066607660866096610661166126613661466156616661766186619662066216622662366246625662666276628662966306631663266336634663566366637663866396640664166426643664466456646664766486649665066516652665366546655665666576658665966606661666266636664666566666667666866696670667166726673667466756676667766786679668066816682668366846685668666876688668966906691669266936694669566966697669866996700670167026703670467056706670767086709671067116712671367146715671667176718671967206721672267236724672567266727672867296730673167326733673467356736673767386739674067416742674367446745674667476748674967506751675267536754675567566757675867596760676167626763676467656766676767686769677067716772677367746775677667776778677967806781678267836784678567866787678867896790679167926793679467956796679767986799680068016802680368046805680668076808680968106811681268136814681568166817681868196820682168226823682468256826682768286829683068316832683368346835683668376838683968406841684268436844684568466847684868496850685168526853685468556856685768586859686068616862686368646865686668676868686968706871687268736874687568766877687868796880688168826883688468856886688768886889689068916892689368946895689668976898689969006901690269036904690569066907690869096910691169126913691469156916691769186919692069216922692369246925692669276928692969306931693269336934693569366937693869396940694169426943694469456946694769486949695069516952695369546955695669576958695969606961696269636964696569666967696869696970697169726973697469756976697769786979698069816982698369846985698669876988698969906991699269936994699569966997699869997000700170027003700470057006700770087009701070117012701370147015701670177018701970207021702270237024702570267027702870297030703170327033703470357036703770387039704070417042704370447045704670477048704970507051705270537054705570567057705870597060706170627063706470657066706770687069707070717072707370747075707670777078707970807081708270837084708570867087708870897090709170927093709470957096709770987099710071017102710371047105710671077108710971107111711271137114711571167117711871197120712171227123712471257126712771287129713071317132713371347135713671377138713971407141714271437144714571467147714871497150715171527153715471557156715771587159716071617162716371647165716671677168716971707171717271737174717571767177717871797180718171827183718471857186718771887189719071917192719371947195719671977198719972007201720272037204720572067207720872097210721172127213721472157216721772187219722072217222722372247225722672277228722972307231723272337234723572367237723872397240724172427243724472457246724772487249725072517252725372547255725672577258725972607261726272637264726572667267726872697270727172727273727472757276727772787279728072817282728372847285728672877288728972907291729272937294729572967297729872997300730173027303730473057306730773087309731073117312731373147315731673177318731973207321732273237324732573267327732873297330733173327333733473357336733773387339734073417342734373447345734673477348734973507351735273537354735573567357735873597360736173627363736473657366736773687369737073717372737373747375737673777378737973807381738273837384738573867387738873897390739173927393739473957396739773987399740074017402740374047405740674077408740974107411741274137414741574167417741874197420742174227423742474257426742774287429743074317432743374347435743674377438743974407441744274437444744574467447
  1. <?php
  2. /**
  3. * PHP-CRUD-API v2 License: MIT
  4. * Maurits van der Schee: maurits@vdschee.nl
  5. * https://github.com/mevdschee/php-crud-api
  6. *
  7. * Dependencies
  8. * - vendor/psr/*: PHP-FIG
  9. * https://github.com/php-fig
  10. * - vendor/nyholm/*: Tobias Nyholm
  11. * https://github.com/Nyholm
  12. **/
  13. namespace Tqdev\PhpCrudApi;
  14. // file: vendor/psr/http-factory/src/RequestFactoryInterface.php
  15. interface RequestFactoryInterface
  16. {
  17. public function createRequest(string $method, $uri): RequestInterface;
  18. }
  19. // file: vendor/psr/http-factory/src/ResponseFactoryInterface.php
  20. interface ResponseFactoryInterface
  21. {
  22. public function createResponse(int $code = 200, string $reasonPhrase = ''): ResponseInterface;
  23. }
  24. // file: vendor/psr/http-factory/src/ServerRequestFactoryInterface.php
  25. interface ServerRequestFactoryInterface
  26. {
  27. public function createServerRequest(string $method, $uri, array $serverParams = []): ServerRequestInterface;
  28. }
  29. // file: vendor/psr/http-factory/src/StreamFactoryInterface.php
  30. interface StreamFactoryInterface
  31. {
  32. public function createStream(string $content = ''): StreamInterface;
  33. public function createStreamFromFile(string $filename, string $mode = 'r'): StreamInterface;
  34. public function createStreamFromResource($resource): StreamInterface;
  35. }
  36. // file: vendor/psr/http-factory/src/UploadedFileFactoryInterface.php
  37. interface UploadedFileFactoryInterface
  38. {
  39. public function createUploadedFile(
  40. StreamInterface $stream,
  41. int $size = null,
  42. int $error = \UPLOAD_ERR_OK,
  43. string $clientFilename = null,
  44. string $clientMediaType = null
  45. ): UploadedFileInterface;
  46. }
  47. // file: vendor/psr/http-factory/src/UriFactoryInterface.php
  48. interface UriFactoryInterface
  49. {
  50. public function createUri(string $uri = ''): UriInterface;
  51. }
  52. // file: vendor/psr/http-message/src/MessageInterface.php
  53. interface MessageInterface
  54. {
  55. public function getProtocolVersion();
  56. public function withProtocolVersion($version);
  57. public function getHeaders();
  58. public function hasHeader($name);
  59. public function getHeader($name);
  60. public function getHeaderLine($name);
  61. public function withHeader($name, $value);
  62. public function withAddedHeader($name, $value);
  63. public function withoutHeader($name);
  64. public function getBody();
  65. public function withBody(StreamInterface $body);
  66. }
  67. // file: vendor/psr/http-message/src/RequestInterface.php
  68. interface RequestInterface extends MessageInterface
  69. {
  70. public function getRequestTarget();
  71. public function withRequestTarget($requestTarget);
  72. public function getMethod();
  73. public function withMethod($method);
  74. public function getUri();
  75. public function withUri(UriInterface $uri, $preserveHost = false);
  76. }
  77. // file: vendor/psr/http-message/src/ResponseInterface.php
  78. interface ResponseInterface extends MessageInterface
  79. {
  80. public function getStatusCode();
  81. public function withStatus($code, $reasonPhrase = '');
  82. public function getReasonPhrase();
  83. }
  84. // file: vendor/psr/http-message/src/ServerRequestInterface.php
  85. interface ServerRequestInterface extends RequestInterface
  86. {
  87. public function getServerParams();
  88. public function getCookieParams();
  89. public function withCookieParams(array $cookies);
  90. public function getQueryParams();
  91. public function withQueryParams(array $query);
  92. public function getUploadedFiles();
  93. public function withUploadedFiles(array $uploadedFiles);
  94. public function getParsedBody();
  95. public function withParsedBody($data);
  96. public function getAttributes();
  97. public function getAttribute($name, $default = null);
  98. public function withAttribute($name, $value);
  99. public function withoutAttribute($name);
  100. }
  101. // file: vendor/psr/http-message/src/StreamInterface.php
  102. interface StreamInterface
  103. {
  104. public function __toString();
  105. public function close();
  106. public function detach();
  107. public function getSize();
  108. public function tell();
  109. public function eof();
  110. public function isSeekable();
  111. public function seek($offset, $whence = SEEK_SET);
  112. public function rewind();
  113. public function isWritable();
  114. public function write($string);
  115. public function isReadable();
  116. public function read($length);
  117. public function getContents();
  118. public function getMetadata($key = null);
  119. }
  120. // file: vendor/psr/http-message/src/UploadedFileInterface.php
  121. interface UploadedFileInterface
  122. {
  123. public function getStream();
  124. public function moveTo($targetPath);
  125. public function getSize();
  126. public function getError();
  127. public function getClientFilename();
  128. public function getClientMediaType();
  129. }
  130. // file: vendor/psr/http-message/src/UriInterface.php
  131. interface UriInterface
  132. {
  133. public function getScheme();
  134. public function getAuthority();
  135. public function getUserInfo();
  136. public function getHost();
  137. public function getPort();
  138. public function getPath();
  139. public function getQuery();
  140. public function getFragment();
  141. public function withScheme($scheme);
  142. public function withUserInfo($user, $password = null);
  143. public function withHost($host);
  144. public function withPort($port);
  145. public function withPath($path);
  146. public function withQuery($query);
  147. public function withFragment($fragment);
  148. public function __toString();
  149. }
  150. // file: vendor/psr/http-server-handler/src/RequestHandlerInterface.php
  151. interface RequestHandlerInterface
  152. {
  153. public function handle(ServerRequestInterface $request): ResponseInterface;
  154. }
  155. // file: vendor/psr/http-server-middleware/src/MiddlewareInterface.php
  156. interface MiddlewareInterface
  157. {
  158. public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface;
  159. }
  160. // file: vendor/nyholm/psr7/src/Factory/Psr17Factory.php
  161. final class Psr17Factory implements RequestFactoryInterface, ResponseFactoryInterface, ServerRequestFactoryInterface, StreamFactoryInterface, UploadedFileFactoryInterface, UriFactoryInterface
  162. {
  163. public function createRequest(string $method, $uri): RequestInterface
  164. {
  165. return new Request($method, $uri);
  166. }
  167. public function createResponse(int $code = 200, string $reasonPhrase = ''): ResponseInterface
  168. {
  169. return new Response($code, [], null, '1.1', $reasonPhrase);
  170. }
  171. public function createStream(string $content = ''): StreamInterface
  172. {
  173. return Stream::create($content);
  174. }
  175. public function createStreamFromFile(string $filename, string $mode = 'r'): StreamInterface
  176. {
  177. $resource = @\fopen($filename, $mode);
  178. if (false === $resource) {
  179. if ('' === $mode || false === \in_array($mode[0], ['r', 'w', 'a', 'x', 'c'])) {
  180. throw new \InvalidArgumentException('The mode ' . $mode . ' is invalid.');
  181. }
  182. throw new \RuntimeException('The file ' . $filename . ' cannot be opened.');
  183. }
  184. return Stream::create($resource);
  185. }
  186. public function createStreamFromResource($resource): StreamInterface
  187. {
  188. return Stream::create($resource);
  189. }
  190. public function createUploadedFile(StreamInterface $stream, int $size = null, int $error = \UPLOAD_ERR_OK, string $clientFilename = null, string $clientMediaType = null): UploadedFileInterface
  191. {
  192. if (null === $size) {
  193. $size = $stream->getSize();
  194. }
  195. return new UploadedFile($stream, $size, $error, $clientFilename, $clientMediaType);
  196. }
  197. public function createUri(string $uri = ''): UriInterface
  198. {
  199. return new Uri($uri);
  200. }
  201. public function createServerRequest(string $method, $uri, array $serverParams = []): ServerRequestInterface
  202. {
  203. return new ServerRequest($method, $uri, [], null, '1.1', $serverParams);
  204. }
  205. }
  206. // file: vendor/nyholm/psr7/src/MessageTrait.php
  207. trait MessageTrait
  208. {
  209. private $headers = [];
  210. private $headerNames = [];
  211. private $protocol = '1.1';
  212. private $stream;
  213. public function getProtocolVersion(): string
  214. {
  215. return $this->protocol;
  216. }
  217. public function withProtocolVersion($version): self
  218. {
  219. if ($this->protocol === $version) {
  220. return $this;
  221. }
  222. $new = clone $this;
  223. $new->protocol = $version;
  224. return $new;
  225. }
  226. public function getHeaders(): array
  227. {
  228. return $this->headers;
  229. }
  230. public function hasHeader($header): bool
  231. {
  232. return isset($this->headerNames[\strtolower($header)]);
  233. }
  234. public function getHeader($header): array
  235. {
  236. $header = \strtolower($header);
  237. if (!isset($this->headerNames[$header])) {
  238. return [];
  239. }
  240. $header = $this->headerNames[$header];
  241. return $this->headers[$header];
  242. }
  243. public function getHeaderLine($header): string
  244. {
  245. return \implode(', ', $this->getHeader($header));
  246. }
  247. public function withHeader($header, $value): self
  248. {
  249. $value = $this->validateAndTrimHeader($header, $value);
  250. $normalized = \strtolower($header);
  251. $new = clone $this;
  252. if (isset($new->headerNames[$normalized])) {
  253. unset($new->headers[$new->headerNames[$normalized]]);
  254. }
  255. $new->headerNames[$normalized] = $header;
  256. $new->headers[$header] = $value;
  257. return $new;
  258. }
  259. public function withAddedHeader($header, $value): self
  260. {
  261. if (!\is_string($header) || '' === $header) {
  262. throw new \InvalidArgumentException('Header name must be an RFC 7230 compatible string.');
  263. }
  264. $new = clone $this;
  265. $new->setHeaders([$header => $value]);
  266. return $new;
  267. }
  268. public function withoutHeader($header): self
  269. {
  270. $normalized = \strtolower($header);
  271. if (!isset($this->headerNames[$normalized])) {
  272. return $this;
  273. }
  274. $header = $this->headerNames[$normalized];
  275. $new = clone $this;
  276. unset($new->headers[$header], $new->headerNames[$normalized]);
  277. return $new;
  278. }
  279. public function getBody(): StreamInterface
  280. {
  281. if (null === $this->stream) {
  282. $this->stream = Stream::create('');
  283. }
  284. return $this->stream;
  285. }
  286. public function withBody(StreamInterface $body): self
  287. {
  288. if ($body === $this->stream) {
  289. return $this;
  290. }
  291. $new = clone $this;
  292. $new->stream = $body;
  293. return $new;
  294. }
  295. private function setHeaders(array $headers): void
  296. {
  297. foreach ($headers as $header => $value) {
  298. $value = $this->validateAndTrimHeader($header, $value);
  299. $normalized = \strtolower($header);
  300. if (isset($this->headerNames[$normalized])) {
  301. $header = $this->headerNames[$normalized];
  302. $this->headers[$header] = \array_merge($this->headers[$header], $value);
  303. } else {
  304. $this->headerNames[$normalized] = $header;
  305. $this->headers[$header] = $value;
  306. }
  307. }
  308. }
  309. private function validateAndTrimHeader($header, $values): array
  310. {
  311. if (!\is_string($header) || 1 !== \preg_match("@^[!#$%&'*+.^_`|~0-9A-Za-z-]+$@", $header)) {
  312. throw new \InvalidArgumentException('Header name must be an RFC 7230 compatible string.');
  313. }
  314. if (!\is_array($values)) {
  315. if ((!\is_numeric($values) && !\is_string($values)) || 1 !== \preg_match("@^[ \t\x21-\x7E\x80-\xFF]*$@", (string) $values)) {
  316. throw new \InvalidArgumentException('Header values must be RFC 7230 compatible strings.');
  317. }
  318. return [\trim((string) $values, " \t")];
  319. }
  320. if (empty($values)) {
  321. throw new \InvalidArgumentException('Header values must be a string or an array of strings, empty array given.');
  322. }
  323. $returnValues = [];
  324. foreach ($values as $v) {
  325. if ((!\is_numeric($v) && !\is_string($v)) || 1 !== \preg_match("@^[ \t\x21-\x7E\x80-\xFF]*$@", (string) $v)) {
  326. throw new \InvalidArgumentException('Header values must be RFC 7230 compatible strings.');
  327. }
  328. $returnValues[] = \trim((string) $v, " \t");
  329. }
  330. return $returnValues;
  331. }
  332. }
  333. // file: vendor/nyholm/psr7/src/Request.php
  334. final class Request implements RequestInterface
  335. {
  336. use MessageTrait;
  337. use RequestTrait;
  338. public function __construct(string $method, $uri, array $headers = [], $body = null, string $version = '1.1')
  339. {
  340. if (!($uri instanceof UriInterface)) {
  341. $uri = new Uri($uri);
  342. }
  343. $this->method = $method;
  344. $this->uri = $uri;
  345. $this->setHeaders($headers);
  346. $this->protocol = $version;
  347. if (!$this->hasHeader('Host')) {
  348. $this->updateHostFromUri();
  349. }
  350. if ('' !== $body && null !== $body) {
  351. $this->stream = Stream::create($body);
  352. }
  353. }
  354. }
  355. // file: vendor/nyholm/psr7/src/RequestTrait.php
  356. trait RequestTrait
  357. {
  358. private $method;
  359. private $requestTarget;
  360. private $uri;
  361. public function getRequestTarget(): string
  362. {
  363. if (null !== $this->requestTarget) {
  364. return $this->requestTarget;
  365. }
  366. if ('' === $target = $this->uri->getPath()) {
  367. $target = '/';
  368. }
  369. if ('' !== $this->uri->getQuery()) {
  370. $target .= '?' . $this->uri->getQuery();
  371. }
  372. return $target;
  373. }
  374. public function withRequestTarget($requestTarget): self
  375. {
  376. if (\preg_match('#\s#', $requestTarget)) {
  377. throw new \InvalidArgumentException('Invalid request target provided; cannot contain whitespace');
  378. }
  379. $new = clone $this;
  380. $new->requestTarget = $requestTarget;
  381. return $new;
  382. }
  383. public function getMethod(): string
  384. {
  385. return $this->method;
  386. }
  387. public function withMethod($method): self
  388. {
  389. if (!\is_string($method)) {
  390. throw new \InvalidArgumentException('Method must be a string');
  391. }
  392. $new = clone $this;
  393. $new->method = $method;
  394. return $new;
  395. }
  396. public function getUri(): UriInterface
  397. {
  398. return $this->uri;
  399. }
  400. public function withUri(UriInterface $uri, $preserveHost = false): self
  401. {
  402. if ($uri === $this->uri) {
  403. return $this;
  404. }
  405. $new = clone $this;
  406. $new->uri = $uri;
  407. if (!$preserveHost || !$this->hasHeader('Host')) {
  408. $new->updateHostFromUri();
  409. }
  410. return $new;
  411. }
  412. private function updateHostFromUri(): void
  413. {
  414. if ('' === $host = $this->uri->getHost()) {
  415. return;
  416. }
  417. if (null !== ($port = $this->uri->getPort())) {
  418. $host .= ':' . $port;
  419. }
  420. if (isset($this->headerNames['host'])) {
  421. $header = $this->headerNames['host'];
  422. } else {
  423. $this->headerNames['host'] = $header = 'Host';
  424. }
  425. $this->headers = [$header => [$host]] + $this->headers;
  426. }
  427. }
  428. // file: vendor/nyholm/psr7/src/Response.php
  429. final class Response implements ResponseInterface
  430. {
  431. use MessageTrait;
  432. private const PHRASES = [
  433. 100 => 'Continue', 101 => 'Switching Protocols', 102 => 'Processing',
  434. 200 => 'OK', 201 => 'Created', 202 => 'Accepted', 203 => 'Non-Authoritative Information', 204 => 'No Content', 205 => 'Reset Content', 206 => 'Partial Content', 207 => 'Multi-status', 208 => 'Already Reported',
  435. 300 => 'Multiple Choices', 301 => 'Moved Permanently', 302 => 'Found', 303 => 'See Other', 304 => 'Not Modified', 305 => 'Use Proxy', 306 => 'Switch Proxy', 307 => 'Temporary Redirect',
  436. 400 => 'Bad Request', 401 => 'Unauthorized', 402 => 'Payment Required', 403 => 'Forbidden', 404 => 'Not Found', 405 => 'Method Not Allowed', 406 => 'Not Acceptable', 407 => 'Proxy Authentication Required', 408 => 'Request Time-out', 409 => 'Conflict', 410 => 'Gone', 411 => 'Length Required', 412 => 'Precondition Failed', 413 => 'Request Entity Too Large', 414 => 'Request-URI Too Large', 415 => 'Unsupported Media Type', 416 => 'Requested range not satisfiable', 417 => 'Expectation Failed', 418 => 'I\'m a teapot', 422 => 'Unprocessable Entity', 423 => 'Locked', 424 => 'Failed Dependency', 425 => 'Unordered Collection', 426 => 'Upgrade Required', 428 => 'Precondition Required', 429 => 'Too Many Requests', 431 => 'Request Header Fields Too Large', 451 => 'Unavailable For Legal Reasons',
  437. 500 => 'Internal Server Error', 501 => 'Not Implemented', 502 => 'Bad Gateway', 503 => 'Service Unavailable', 504 => 'Gateway Time-out', 505 => 'HTTP Version not supported', 506 => 'Variant Also Negotiates', 507 => 'Insufficient Storage', 508 => 'Loop Detected', 511 => 'Network Authentication Required',
  438. ];
  439. private $reasonPhrase = '';
  440. private $statusCode;
  441. public function __construct(int $status = 200, array $headers = [], $body = null, string $version = '1.1', string $reason = null)
  442. {
  443. if ('' !== $body && null !== $body) {
  444. $this->stream = Stream::create($body);
  445. }
  446. $this->statusCode = $status;
  447. $this->setHeaders($headers);
  448. if (null === $reason && isset(self::PHRASES[$this->statusCode])) {
  449. $this->reasonPhrase = self::PHRASES[$status];
  450. } else {
  451. $this->reasonPhrase = $reason;
  452. }
  453. $this->protocol = $version;
  454. }
  455. public function getStatusCode(): int
  456. {
  457. return $this->statusCode;
  458. }
  459. public function getReasonPhrase(): string
  460. {
  461. return $this->reasonPhrase;
  462. }
  463. public function withStatus($code, $reasonPhrase = ''): self
  464. {
  465. if (!\is_int($code) && !\is_string($code)) {
  466. throw new \InvalidArgumentException('Status code has to be an integer');
  467. }
  468. $code = (int) $code;
  469. if ($code < 100 || $code > 599) {
  470. throw new \InvalidArgumentException('Status code has to be an integer between 100 and 599');
  471. }
  472. $new = clone $this;
  473. $new->statusCode = $code;
  474. if ((null === $reasonPhrase || '' === $reasonPhrase) && isset(self::PHRASES[$new->statusCode])) {
  475. $reasonPhrase = self::PHRASES[$new->statusCode];
  476. }
  477. $new->reasonPhrase = $reasonPhrase;
  478. return $new;
  479. }
  480. }
  481. // file: vendor/nyholm/psr7/src/ServerRequest.php
  482. final class ServerRequest implements ServerRequestInterface
  483. {
  484. use MessageTrait;
  485. use RequestTrait;
  486. private $attributes = [];
  487. private $cookieParams = [];
  488. private $parsedBody;
  489. private $queryParams = [];
  490. private $serverParams;
  491. private $uploadedFiles = [];
  492. public function __construct(string $method, $uri, array $headers = [], $body = null, string $version = '1.1', array $serverParams = [])
  493. {
  494. $this->serverParams = $serverParams;
  495. if (!($uri instanceof UriInterface)) {
  496. $uri = new Uri($uri);
  497. }
  498. $this->method = $method;
  499. $this->uri = $uri;
  500. $this->setHeaders($headers);
  501. $this->protocol = $version;
  502. if (!$this->hasHeader('Host')) {
  503. $this->updateHostFromUri();
  504. }
  505. if ('' !== $body && null !== $body) {
  506. $this->stream = Stream::create($body);
  507. }
  508. }
  509. public function getServerParams(): array
  510. {
  511. return $this->serverParams;
  512. }
  513. public function getUploadedFiles(): array
  514. {
  515. return $this->uploadedFiles;
  516. }
  517. public function withUploadedFiles(array $uploadedFiles)
  518. {
  519. $new = clone $this;
  520. $new->uploadedFiles = $uploadedFiles;
  521. return $new;
  522. }
  523. public function getCookieParams(): array
  524. {
  525. return $this->cookieParams;
  526. }
  527. public function withCookieParams(array $cookies)
  528. {
  529. $new = clone $this;
  530. $new->cookieParams = $cookies;
  531. return $new;
  532. }
  533. public function getQueryParams(): array
  534. {
  535. return $this->queryParams;
  536. }
  537. public function withQueryParams(array $query)
  538. {
  539. $new = clone $this;
  540. $new->queryParams = $query;
  541. return $new;
  542. }
  543. public function getParsedBody()
  544. {
  545. return $this->parsedBody;
  546. }
  547. public function withParsedBody($data)
  548. {
  549. if (!\is_array($data) && !\is_object($data) && null !== $data) {
  550. throw new \InvalidArgumentException('First parameter to withParsedBody MUST be object, array or null');
  551. }
  552. $new = clone $this;
  553. $new->parsedBody = $data;
  554. return $new;
  555. }
  556. public function getAttributes(): array
  557. {
  558. return $this->attributes;
  559. }
  560. public function getAttribute($attribute, $default = null)
  561. {
  562. if (false === \array_key_exists($attribute, $this->attributes)) {
  563. return $default;
  564. }
  565. return $this->attributes[$attribute];
  566. }
  567. public function withAttribute($attribute, $value): self
  568. {
  569. $new = clone $this;
  570. $new->attributes[$attribute] = $value;
  571. return $new;
  572. }
  573. public function withoutAttribute($attribute): self
  574. {
  575. if (false === \array_key_exists($attribute, $this->attributes)) {
  576. return $this;
  577. }
  578. $new = clone $this;
  579. unset($new->attributes[$attribute]);
  580. return $new;
  581. }
  582. }
  583. // file: vendor/nyholm/psr7/src/Stream.php
  584. final class Stream implements StreamInterface
  585. {
  586. private $stream;
  587. private $seekable;
  588. private $readable;
  589. private $writable;
  590. private $uri;
  591. private $size;
  592. private const READ_WRITE_HASH = [
  593. 'read' => [
  594. 'r' => true, 'w+' => true, 'r+' => true, 'x+' => true, 'c+' => true,
  595. 'rb' => true, 'w+b' => true, 'r+b' => true, 'x+b' => true,
  596. 'c+b' => true, 'rt' => true, 'w+t' => true, 'r+t' => true,
  597. 'x+t' => true, 'c+t' => true, 'a+' => true,
  598. ],
  599. 'write' => [
  600. 'w' => true, 'w+' => true, 'rw' => true, 'r+' => true, 'x+' => true,
  601. 'c+' => true, 'wb' => true, 'w+b' => true, 'r+b' => true,
  602. 'x+b' => true, 'c+b' => true, 'w+t' => true, 'r+t' => true,
  603. 'x+t' => true, 'c+t' => true, 'a' => true, 'a+' => true,
  604. ],
  605. ];
  606. private function __construct()
  607. {
  608. }
  609. public static function create($body = ''): StreamInterface
  610. {
  611. if ($body instanceof StreamInterface) {
  612. return $body;
  613. }
  614. if (\is_string($body)) {
  615. $resource = \fopen('php://temp', 'rw+');
  616. \fwrite($resource, $body);
  617. $body = $resource;
  618. }
  619. if (\is_resource($body)) {
  620. $new = new self();
  621. $new->stream = $body;
  622. $meta = \stream_get_meta_data($new->stream);
  623. $new->seekable = $meta['seekable'];
  624. $new->readable = isset(self::READ_WRITE_HASH['read'][$meta['mode']]);
  625. $new->writable = isset(self::READ_WRITE_HASH['write'][$meta['mode']]);
  626. $new->uri = $new->getMetadata('uri');
  627. return $new;
  628. }
  629. throw new \InvalidArgumentException('First argument to Stream::create() must be a string, resource or StreamInterface.');
  630. }
  631. public function __destruct()
  632. {
  633. $this->close();
  634. }
  635. public function __toString(): string
  636. {
  637. try {
  638. if ($this->isSeekable()) {
  639. $this->seek(0);
  640. }
  641. return $this->getContents();
  642. } catch (\Exception $e) {
  643. return '';
  644. }
  645. }
  646. public function close(): void
  647. {
  648. if (isset($this->stream)) {
  649. if (\is_resource($this->stream)) {
  650. \fclose($this->stream);
  651. }
  652. $this->detach();
  653. }
  654. }
  655. public function detach()
  656. {
  657. if (!isset($this->stream)) {
  658. return null;
  659. }
  660. $result = $this->stream;
  661. unset($this->stream);
  662. $this->size = $this->uri = null;
  663. $this->readable = $this->writable = $this->seekable = false;
  664. return $result;
  665. }
  666. public function getSize(): ?int
  667. {
  668. if (null !== $this->size) {
  669. return $this->size;
  670. }
  671. if (!isset($this->stream)) {
  672. return null;
  673. }
  674. if ($this->uri) {
  675. \clearstatcache(true, $this->uri);
  676. }
  677. $stats = \fstat($this->stream);
  678. if (isset($stats['size'])) {
  679. $this->size = $stats['size'];
  680. return $this->size;
  681. }
  682. return null;
  683. }
  684. public function tell(): int
  685. {
  686. if (false === $result = \ftell($this->stream)) {
  687. throw new \RuntimeException('Unable to determine stream position');
  688. }
  689. return $result;
  690. }
  691. public function eof(): bool
  692. {
  693. return !$this->stream || \feof($this->stream);
  694. }
  695. public function isSeekable(): bool
  696. {
  697. return $this->seekable;
  698. }
  699. public function seek($offset, $whence = \SEEK_SET): void
  700. {
  701. if (!$this->seekable) {
  702. throw new \RuntimeException('Stream is not seekable');
  703. }
  704. if (-1 === \fseek($this->stream, $offset, $whence)) {
  705. throw new \RuntimeException('Unable to seek to stream position ' . $offset . ' with whence ' . \var_export($whence, true));
  706. }
  707. }
  708. public function rewind(): void
  709. {
  710. $this->seek(0);
  711. }
  712. public function isWritable(): bool
  713. {
  714. return $this->writable;
  715. }
  716. public function write($string): int
  717. {
  718. if (!$this->writable) {
  719. throw new \RuntimeException('Cannot write to a non-writable stream');
  720. }
  721. $this->size = null;
  722. if (false === $result = \fwrite($this->stream, $string)) {
  723. throw new \RuntimeException('Unable to write to stream');
  724. }
  725. return $result;
  726. }
  727. public function isReadable(): bool
  728. {
  729. return $this->readable;
  730. }
  731. public function read($length): string
  732. {
  733. if (!$this->readable) {
  734. throw new \RuntimeException('Cannot read from non-readable stream');
  735. }
  736. return \fread($this->stream, $length);
  737. }
  738. public function getContents(): string
  739. {
  740. if (!isset($this->stream)) {
  741. throw new \RuntimeException('Unable to read stream contents');
  742. }
  743. if (false === $contents = \stream_get_contents($this->stream)) {
  744. throw new \RuntimeException('Unable to read stream contents');
  745. }
  746. return $contents;
  747. }
  748. public function getMetadata($key = null)
  749. {
  750. if (!isset($this->stream)) {
  751. return $key ? null : [];
  752. }
  753. $meta = \stream_get_meta_data($this->stream);
  754. if (null === $key) {
  755. return $meta;
  756. }
  757. return $meta[$key] ?? null;
  758. }
  759. }
  760. // file: vendor/nyholm/psr7/src/UploadedFile.php
  761. final class UploadedFile implements UploadedFileInterface
  762. {
  763. private const ERRORS = [
  764. \UPLOAD_ERR_OK => 1,
  765. \UPLOAD_ERR_INI_SIZE => 1,
  766. \UPLOAD_ERR_FORM_SIZE => 1,
  767. \UPLOAD_ERR_PARTIAL => 1,
  768. \UPLOAD_ERR_NO_FILE => 1,
  769. \UPLOAD_ERR_NO_TMP_DIR => 1,
  770. \UPLOAD_ERR_CANT_WRITE => 1,
  771. \UPLOAD_ERR_EXTENSION => 1,
  772. ];
  773. private $clientFilename;
  774. private $clientMediaType;
  775. private $error;
  776. private $file;
  777. private $moved = false;
  778. private $size;
  779. private $stream;
  780. public function __construct($streamOrFile, $size, $errorStatus, $clientFilename = null, $clientMediaType = null)
  781. {
  782. if (false === \is_int($errorStatus) || !isset(self::ERRORS[$errorStatus])) {
  783. throw new \InvalidArgumentException('Upload file error status must be an integer value and one of the "UPLOAD_ERR_*" constants.');
  784. }
  785. if (false === \is_int($size)) {
  786. throw new \InvalidArgumentException('Upload file size must be an integer');
  787. }
  788. if (null !== $clientFilename && !\is_string($clientFilename)) {
  789. throw new \InvalidArgumentException('Upload file client filename must be a string or null');
  790. }
  791. if (null !== $clientMediaType && !\is_string($clientMediaType)) {
  792. throw new \InvalidArgumentException('Upload file client media type must be a string or null');
  793. }
  794. $this->error = $errorStatus;
  795. $this->size = $size;
  796. $this->clientFilename = $clientFilename;
  797. $this->clientMediaType = $clientMediaType;
  798. if (\UPLOAD_ERR_OK === $this->error) {
  799. if (\is_string($streamOrFile)) {
  800. $this->file = $streamOrFile;
  801. } elseif (\is_resource($streamOrFile)) {
  802. $this->stream = Stream::create($streamOrFile);
  803. } elseif ($streamOrFile instanceof StreamInterface) {
  804. $this->stream = $streamOrFile;
  805. } else {
  806. throw new \InvalidArgumentException('Invalid stream or file provided for UploadedFile');
  807. }
  808. }
  809. }
  810. private function validateActive(): void
  811. {
  812. if (\UPLOAD_ERR_OK !== $this->error) {
  813. throw new \RuntimeException('Cannot retrieve stream due to upload error');
  814. }
  815. if ($this->moved) {
  816. throw new \RuntimeException('Cannot retrieve stream after it has already been moved');
  817. }
  818. }
  819. public function getStream(): StreamInterface
  820. {
  821. $this->validateActive();
  822. if ($this->stream instanceof StreamInterface) {
  823. return $this->stream;
  824. }
  825. $resource = \fopen($this->file, 'r');
  826. return Stream::create($resource);
  827. }
  828. public function moveTo($targetPath): void
  829. {
  830. $this->validateActive();
  831. if (!\is_string($targetPath) || '' === $targetPath) {
  832. throw new \InvalidArgumentException('Invalid path provided for move operation; must be a non-empty string');
  833. }
  834. if (null !== $this->file) {
  835. $this->moved = 'cli' === \PHP_SAPI ? \rename($this->file, $targetPath) : \move_uploaded_file($this->file, $targetPath);
  836. } else {
  837. $stream = $this->getStream();
  838. if ($stream->isSeekable()) {
  839. $stream->rewind();
  840. }
  841. $dest = Stream::create(\fopen($targetPath, 'w'));
  842. while (!$stream->eof()) {
  843. if (!$dest->write($stream->read(1048576))) {
  844. break;
  845. }
  846. }
  847. $this->moved = true;
  848. }
  849. if (false === $this->moved) {
  850. throw new \RuntimeException(\sprintf('Uploaded file could not be moved to %s', $targetPath));
  851. }
  852. }
  853. public function getSize(): int
  854. {
  855. return $this->size;
  856. }
  857. public function getError(): int
  858. {
  859. return $this->error;
  860. }
  861. public function getClientFilename(): ?string
  862. {
  863. return $this->clientFilename;
  864. }
  865. public function getClientMediaType(): ?string
  866. {
  867. return $this->clientMediaType;
  868. }
  869. }
  870. // file: vendor/nyholm/psr7/src/Uri.php
  871. final class Uri implements UriInterface
  872. {
  873. private const SCHEMES = ['http' => 80, 'https' => 443];
  874. private const CHAR_UNRESERVED = 'a-zA-Z0-9_\-\.~';
  875. private const CHAR_SUB_DELIMS = '!\$&\'\(\)\*\+,;=';
  876. private $scheme = '';
  877. private $userInfo = '';
  878. private $host = '';
  879. private $port;
  880. private $path = '';
  881. private $query = '';
  882. private $fragment = '';
  883. public function __construct(string $uri = '')
  884. {
  885. if ('' !== $uri) {
  886. if (false === $parts = \parse_url($uri)) {
  887. throw new \InvalidArgumentException("Unable to parse URI: $uri");
  888. }
  889. $this->scheme = isset($parts['scheme']) ? \strtolower($parts['scheme']) : '';
  890. $this->userInfo = $parts['user'] ?? '';
  891. $this->host = isset($parts['host']) ? \strtolower($parts['host']) : '';
  892. $this->port = isset($parts['port']) ? $this->filterPort($parts['port']) : null;
  893. $this->path = isset($parts['path']) ? $this->filterPath($parts['path']) : '';
  894. $this->query = isset($parts['query']) ? $this->filterQueryAndFragment($parts['query']) : '';
  895. $this->fragment = isset($parts['fragment']) ? $this->filterQueryAndFragment($parts['fragment']) : '';
  896. if (isset($parts['pass'])) {
  897. $this->userInfo .= ':' . $parts['pass'];
  898. }
  899. }
  900. }
  901. public function __toString(): string
  902. {
  903. return self::createUriString($this->scheme, $this->getAuthority(), $this->path, $this->query, $this->fragment);
  904. }
  905. public function getScheme(): string
  906. {
  907. return $this->scheme;
  908. }
  909. public function getAuthority(): string
  910. {
  911. if ('' === $this->host) {
  912. return '';
  913. }
  914. $authority = $this->host;
  915. if ('' !== $this->userInfo) {
  916. $authority = $this->userInfo . '@' . $authority;
  917. }
  918. if (null !== $this->port) {
  919. $authority .= ':' . $this->port;
  920. }
  921. return $authority;
  922. }
  923. public function getUserInfo(): string
  924. {
  925. return $this->userInfo;
  926. }
  927. public function getHost(): string
  928. {
  929. return $this->host;
  930. }
  931. public function getPort(): ?int
  932. {
  933. return $this->port;
  934. }
  935. public function getPath(): string
  936. {
  937. return $this->path;
  938. }
  939. public function getQuery(): string
  940. {
  941. return $this->query;
  942. }
  943. public function getFragment(): string
  944. {
  945. return $this->fragment;
  946. }
  947. public function withScheme($scheme): self
  948. {
  949. if (!\is_string($scheme)) {
  950. throw new \InvalidArgumentException('Scheme must be a string');
  951. }
  952. if ($this->scheme === $scheme = \strtolower($scheme)) {
  953. return $this;
  954. }
  955. $new = clone $this;
  956. $new->scheme = $scheme;
  957. $new->port = $new->filterPort($new->port);
  958. return $new;
  959. }
  960. public function withUserInfo($user, $password = null): self
  961. {
  962. $info = $user;
  963. if (null !== $password && '' !== $password) {
  964. $info .= ':' . $password;
  965. }
  966. if ($this->userInfo === $info) {
  967. return $this;
  968. }
  969. $new = clone $this;
  970. $new->userInfo = $info;
  971. return $new;
  972. }
  973. public function withHost($host): self
  974. {
  975. if (!\is_string($host)) {
  976. throw new \InvalidArgumentException('Host must be a string');
  977. }
  978. if ($this->host === $host = \strtolower($host)) {
  979. return $this;
  980. }
  981. $new = clone $this;
  982. $new->host = $host;
  983. return $new;
  984. }
  985. public function withPort($port): self
  986. {
  987. if ($this->port === $port = $this->filterPort($port)) {
  988. return $this;
  989. }
  990. $new = clone $this;
  991. $new->port = $port;
  992. return $new;
  993. }
  994. public function withPath($path): self
  995. {
  996. if ($this->path === $path = $this->filterPath($path)) {
  997. return $this;
  998. }
  999. $new = clone $this;
  1000. $new->path = $path;
  1001. return $new;
  1002. }
  1003. public function withQuery($query): self
  1004. {
  1005. if ($this->query === $query = $this->filterQueryAndFragment($query)) {
  1006. return $this;
  1007. }
  1008. $new = clone $this;
  1009. $new->query = $query;
  1010. return $new;
  1011. }
  1012. public function withFragment($fragment): self
  1013. {
  1014. if ($this->fragment === $fragment = $this->filterQueryAndFragment($fragment)) {
  1015. return $this;
  1016. }
  1017. $new = clone $this;
  1018. $new->fragment = $fragment;
  1019. return $new;
  1020. }
  1021. private static function createUriString(string $scheme, string $authority, string $path, string $query, string $fragment): string
  1022. {
  1023. $uri = '';
  1024. if ('' !== $scheme) {
  1025. $uri .= $scheme . ':';
  1026. }
  1027. if ('' !== $authority) {
  1028. $uri .= '//' . $authority;
  1029. }
  1030. if ('' !== $path) {
  1031. if ('/' !== $path[0]) {
  1032. if ('' !== $authority) {
  1033. $path = '/' . $path;
  1034. }
  1035. } elseif (isset($path[1]) && '/' === $path[1]) {
  1036. if ('' === $authority) {
  1037. $path = '/' . \ltrim($path, '/');
  1038. }
  1039. }
  1040. $uri .= $path;
  1041. }
  1042. if ('' !== $query) {
  1043. $uri .= '?' . $query;
  1044. }
  1045. if ('' !== $fragment) {
  1046. $uri .= '#' . $fragment;
  1047. }
  1048. return $uri;
  1049. }
  1050. private static function isNonStandardPort(string $scheme, int $port): bool
  1051. {
  1052. return !isset(self::SCHEMES[$scheme]) || $port !== self::SCHEMES[$scheme];
  1053. }
  1054. private function filterPort($port): ?int
  1055. {
  1056. if (null === $port) {
  1057. return null;
  1058. }
  1059. $port = (int) $port;
  1060. if (1 > $port || 0xffff < $port) {
  1061. throw new \InvalidArgumentException(\sprintf('Invalid port: %d. Must be between 1 and 65535', $port));
  1062. }
  1063. return self::isNonStandardPort($this->scheme, $port) ? $port : null;
  1064. }
  1065. private function filterPath($path): string
  1066. {
  1067. if (!\is_string($path)) {
  1068. throw new \InvalidArgumentException('Path must be a string');
  1069. }
  1070. return \preg_replace_callback('/(?:[^' . self::CHAR_UNRESERVED . self::CHAR_SUB_DELIMS . '%:@\/]++|%(?![A-Fa-f0-9]{2}))/', [__CLASS__, 'rawurlencodeMatchZero'], $path);
  1071. }
  1072. private function filterQueryAndFragment($str): string
  1073. {
  1074. if (!\is_string($str)) {
  1075. throw new \InvalidArgumentException('Query and fragment must be a string');
  1076. }
  1077. return \preg_replace_callback('/(?:[^' . self::CHAR_UNRESERVED . self::CHAR_SUB_DELIMS . '%:@\/\?]++|%(?![A-Fa-f0-9]{2}))/', [__CLASS__, 'rawurlencodeMatchZero'], $str);
  1078. }
  1079. private static function rawurlencodeMatchZero(array $match): string
  1080. {
  1081. return \rawurlencode($match[0]);
  1082. }
  1083. }
  1084. // file: vendor/nyholm/psr7-server/src/ServerRequestCreator.php
  1085. final class ServerRequestCreator implements ServerRequestCreatorInterface
  1086. {
  1087. private $serverRequestFactory;
  1088. private $uriFactory;
  1089. private $uploadedFileFactory;
  1090. private $streamFactory;
  1091. public function __construct(
  1092. ServerRequestFactoryInterface $serverRequestFactory,
  1093. UriFactoryInterface $uriFactory,
  1094. UploadedFileFactoryInterface $uploadedFileFactory,
  1095. StreamFactoryInterface $streamFactory
  1096. ) {
  1097. $this->serverRequestFactory = $serverRequestFactory;
  1098. $this->uriFactory = $uriFactory;
  1099. $this->uploadedFileFactory = $uploadedFileFactory;
  1100. $this->streamFactory = $streamFactory;
  1101. }
  1102. public function fromGlobals(): ServerRequestInterface
  1103. {
  1104. $server = $_SERVER;
  1105. if (false === isset($server['REQUEST_METHOD'])) {
  1106. $server['REQUEST_METHOD'] = 'GET';
  1107. }
  1108. $headers = \function_exists('getallheaders') ? getallheaders() : static::getHeadersFromServer($_SERVER);
  1109. return $this->fromArrays($server, $headers, $_COOKIE, $_GET, $_POST, $_FILES, fopen('php://input', 'r') ?: null);
  1110. }
  1111. public function fromArrays(array $server, array $headers = [], array $cookie = [], array $get = [], array $post = [], array $files = [], $body = null): ServerRequestInterface
  1112. {
  1113. $method = $this->getMethodFromEnv($server);
  1114. $uri = $this->getUriFromEnvWithHTTP($server);
  1115. $protocol = isset($server['SERVER_PROTOCOL']) ? \str_replace('HTTP/', '', $server['SERVER_PROTOCOL']) : '1.1';
  1116. $serverRequest = $this->serverRequestFactory->createServerRequest($method, $uri, $server);
  1117. foreach ($headers as $name => $value) {
  1118. $serverRequest = $serverRequest->withAddedHeader($name, $value);
  1119. }
  1120. $serverRequest = $serverRequest
  1121. ->withProtocolVersion($protocol)
  1122. ->withCookieParams($cookie)
  1123. ->withQueryParams($get)
  1124. ->withParsedBody($post)
  1125. ->withUploadedFiles($this->normalizeFiles($files));
  1126. if (null === $body) {
  1127. return $serverRequest;
  1128. }
  1129. if (\is_resource($body)) {
  1130. $body = $this->streamFactory->createStreamFromResource($body);
  1131. } elseif (\is_string($body)) {
  1132. $body = $this->streamFactory->createStream($body);
  1133. } elseif (!$body instanceof StreamInterface) {
  1134. throw new \InvalidArgumentException('The $body parameter to ServerRequestCreator::fromArrays must be string, resource or StreamInterface');
  1135. }
  1136. return $serverRequest->withBody($body);
  1137. }
  1138. public static function getHeadersFromServer(array $server): array
  1139. {
  1140. $headers = [];
  1141. foreach ($server as $key => $value) {
  1142. if (0 === \strpos($key, 'REDIRECT_')) {
  1143. $key = \substr($key, 9);
  1144. if (\array_key_exists($key, $server)) {
  1145. continue;
  1146. }
  1147. }
  1148. if ($value && 0 === \strpos($key, 'HTTP_')) {
  1149. $name = \strtr(\strtolower(\substr($key, 5)), '_', '-');
  1150. $headers[$name] = $value;
  1151. continue;
  1152. }
  1153. if ($value && 0 === \strpos($key, 'CONTENT_')) {
  1154. $name = 'content-'.\strtolower(\substr($key, 8));
  1155. $headers[$name] = $value;
  1156. continue;
  1157. }
  1158. }
  1159. return $headers;
  1160. }
  1161. private function getMethodFromEnv(array $environment): string
  1162. {
  1163. if (false === isset($environment['REQUEST_METHOD'])) {
  1164. throw new \InvalidArgumentException('Cannot determine HTTP method');
  1165. }
  1166. return $environment['REQUEST_METHOD'];
  1167. }
  1168. private function getUriFromEnvWithHTTP(array $environment): UriInterface
  1169. {
  1170. $uri = $this->createUriFromArray($environment);
  1171. if (empty($uri->getScheme())) {
  1172. $uri = $uri->withScheme('http');
  1173. }
  1174. return $uri;
  1175. }
  1176. private function normalizeFiles(array $files): array
  1177. {
  1178. $normalized = [];
  1179. foreach ($files as $key => $value) {
  1180. if ($value instanceof UploadedFileInterface) {
  1181. $normalized[$key] = $value;
  1182. } elseif (\is_array($value) && isset($value['tmp_name'])) {
  1183. $normalized[$key] = $this->createUploadedFileFromSpec($value);
  1184. } elseif (\is_array($value)) {
  1185. $normalized[$key] = $this->normalizeFiles($value);
  1186. } else {
  1187. throw new \InvalidArgumentException('Invalid value in files specification');
  1188. }
  1189. }
  1190. return $normalized;
  1191. }
  1192. private function createUploadedFileFromSpec(array $value)
  1193. {
  1194. if (\is_array($value['tmp_name'])) {
  1195. return $this->normalizeNestedFileSpec($value);
  1196. }
  1197. try {
  1198. $stream = $this->streamFactory->createStreamFromFile($value['tmp_name']);
  1199. } catch (\RuntimeException $e) {
  1200. $stream = $this->streamFactory->createStream();
  1201. }
  1202. return $this->uploadedFileFactory->createUploadedFile(
  1203. $stream,
  1204. (int) $value['size'],
  1205. (int) $value['error'],
  1206. $value['name'],
  1207. $value['type']
  1208. );
  1209. }
  1210. private function normalizeNestedFileSpec(array $files = []): array
  1211. {
  1212. $normalizedFiles = [];
  1213. foreach (\array_keys($files['tmp_name']) as $key) {
  1214. $spec = [
  1215. 'tmp_name' => $files['tmp_name'][$key],
  1216. 'size' => $files['size'][$key],
  1217. 'error' => $files['error'][$key],
  1218. 'name' => $files['name'][$key],
  1219. 'type' => $files['type'][$key],
  1220. ];
  1221. $normalizedFiles[$key] = $this->createUploadedFileFromSpec($spec);
  1222. }
  1223. return $normalizedFiles;
  1224. }
  1225. private function createUriFromArray(array $server): UriInterface
  1226. {
  1227. $uri = $this->uriFactory->createUri('');
  1228. if (isset($server['REQUEST_SCHEME'])) {
  1229. $uri = $uri->withScheme($server['REQUEST_SCHEME']);
  1230. } elseif (isset($server['HTTPS'])) {
  1231. $uri = $uri->withScheme('on' === $server['HTTPS'] ? 'https' : 'http');
  1232. }
  1233. if (isset($server['HTTP_HOST'])) {
  1234. $uri = $uri->withHost($server['HTTP_HOST']);
  1235. } elseif (isset($server['SERVER_NAME'])) {
  1236. $uri = $uri->withHost($server['SERVER_NAME']);
  1237. }
  1238. if (isset($server['SERVER_PORT'])) {
  1239. $uri = $uri->withPort($server['SERVER_PORT']);
  1240. }
  1241. if (isset($server['REQUEST_URI'])) {
  1242. $uri = $uri->withPath(\current(\explode('?', $server['REQUEST_URI'])));
  1243. }
  1244. if (isset($server['QUERY_STRING'])) {
  1245. $uri = $uri->withQuery($server['QUERY_STRING']);
  1246. }
  1247. return $uri;
  1248. }
  1249. }
  1250. // file: vendor/nyholm/psr7-server/src/ServerRequestCreatorInterface.php
  1251. interface ServerRequestCreatorInterface
  1252. {
  1253. public function fromGlobals(): ServerRequestInterface;
  1254. public function fromArrays(
  1255. array $server,
  1256. array $headers = [],
  1257. array $cookie = [],
  1258. array $get = [],
  1259. array $post = [],
  1260. array $files = [],
  1261. $body = null
  1262. ): ServerRequestInterface;
  1263. public static function getHeadersFromServer(array $server): array;
  1264. }
  1265. // file: src/Tqdev/PhpCrudApi/Cache/Cache.php
  1266. interface Cache
  1267. {
  1268. public function set(string $key, string $value, int $ttl = 0): bool;
  1269. public function get(string $key): string;
  1270. public function clear(): bool;
  1271. }
  1272. // file: src/Tqdev/PhpCrudApi/Cache/CacheFactory.php
  1273. class CacheFactory
  1274. {
  1275. const PREFIX = 'phpcrudapi-%s-%s-%s-';
  1276. private static function getPrefix(Config $config): string
  1277. {
  1278. $driver = $config->getDriver();
  1279. $database = $config->getDatabase();
  1280. $filehash = substr(md5(__FILE__), 0, 8);
  1281. return sprintf(self::PREFIX, $driver, $database, $filehash);
  1282. }
  1283. public static function create(Config $config): Cache
  1284. {
  1285. switch ($config->getCacheType()) {
  1286. case 'TempFile':
  1287. $cache = new TempFileCache(self::getPrefix($config), $config->getCachePath());
  1288. break;
  1289. case 'Redis':
  1290. $cache = new RedisCache(self::getPrefix($config), $config->getCachePath());
  1291. break;
  1292. case 'Memcache':
  1293. $cache = new MemcacheCache(self::getPrefix($config), $config->getCachePath());
  1294. break;
  1295. case 'Memcached':
  1296. $cache = new MemcachedCache(self::getPrefix($config), $config->getCachePath());
  1297. break;
  1298. default:
  1299. $cache = new NoCache();
  1300. }
  1301. return $cache;
  1302. }
  1303. }
  1304. // file: src/Tqdev/PhpCrudApi/Cache/MemcacheCache.php
  1305. class MemcacheCache implements Cache
  1306. {
  1307. protected $prefix;
  1308. protected $memcache;
  1309. public function __construct(string $prefix, string $config)
  1310. {
  1311. $this->prefix = $prefix;
  1312. if ($config == '') {
  1313. $address = 'localhost';
  1314. $port = 11211;
  1315. } elseif (strpos($config, ':') === false) {
  1316. $address = $config;
  1317. $port = 11211;
  1318. } else {
  1319. list($address, $port) = explode(':', $config);
  1320. }
  1321. $this->memcache = $this->create();
  1322. $this->memcache->addServer($address, $port);
  1323. }
  1324. protected function create() /*: \Memcache*/
  1325. {
  1326. return new \Memcache();
  1327. }
  1328. public function set(string $key, string $value, int $ttl = 0): bool
  1329. {
  1330. return $this->memcache->set($this->prefix . $key, $value, 0, $ttl);
  1331. }
  1332. public function get(string $key): string
  1333. {
  1334. return $this->memcache->get($this->prefix . $key) ?: '';
  1335. }
  1336. public function clear(): bool
  1337. {
  1338. return $this->memcache->flush();
  1339. }
  1340. }
  1341. // file: src/Tqdev/PhpCrudApi/Cache/MemcachedCache.php
  1342. class MemcachedCache extends MemcacheCache
  1343. {
  1344. protected function create() /*: \Memcached*/
  1345. {
  1346. return new \Memcached();
  1347. }
  1348. public function set(string $key, string $value, int $ttl = 0): bool
  1349. {
  1350. return $this->memcache->set($this->prefix . $key, $value, $ttl);
  1351. }
  1352. }
  1353. // file: src/Tqdev/PhpCrudApi/Cache/NoCache.php
  1354. class NoCache implements Cache
  1355. {
  1356. public function __construct()
  1357. {
  1358. }
  1359. public function set(string $key, string $value, int $ttl = 0): bool
  1360. {
  1361. return true;
  1362. }
  1363. public function get(string $key): string
  1364. {
  1365. return '';
  1366. }
  1367. public function clear(): bool
  1368. {
  1369. return true;
  1370. }
  1371. }
  1372. // file: src/Tqdev/PhpCrudApi/Cache/RedisCache.php
  1373. class RedisCache implements Cache
  1374. {
  1375. protected $prefix;
  1376. protected $redis;
  1377. public function __construct(string $prefix, string $config)
  1378. {
  1379. $this->prefix = $prefix;
  1380. if ($config == '') {
  1381. $config = '127.0.0.1';
  1382. }
  1383. $params = explode(':', $config, 6);
  1384. if (isset($params[3])) {
  1385. $params[3] = null;
  1386. }
  1387. $this->redis = new \Redis();
  1388. call_user_func_array(array($this->redis, 'pconnect'), $params);
  1389. }
  1390. public function set(string $key, string $value, int $ttl = 0): bool
  1391. {
  1392. return $this->redis->set($this->prefix . $key, $value, $ttl);
  1393. }
  1394. public function get(string $key): string
  1395. {
  1396. return $this->redis->get($this->prefix . $key) ?: '';
  1397. }
  1398. public function clear(): bool
  1399. {
  1400. return $this->redis->flushDb();
  1401. }
  1402. }
  1403. // file: src/Tqdev/PhpCrudApi/Cache/TempFileCache.php
  1404. class TempFileCache implements Cache
  1405. {
  1406. const SUFFIX = 'cache';
  1407. private $path;
  1408. private $segments;
  1409. public function __construct(string $prefix, string $config)
  1410. {
  1411. $this->segments = [];
  1412. $s = DIRECTORY_SEPARATOR;
  1413. $ps = PATH_SEPARATOR;
  1414. if ($config == '') {
  1415. $this->path = sys_get_temp_dir() . $s . $prefix . self::SUFFIX;
  1416. } elseif (strpos($config, $ps) === false) {
  1417. $this->path = $config;
  1418. } else {
  1419. list($path, $segments) = explode($ps, $config);
  1420. $this->path = $path;
  1421. $this->segments = explode(',', $segments);
  1422. }
  1423. if (file_exists($this->path) && is_dir($this->path)) {
  1424. $this->clean($this->path, array_filter($this->segments), strlen(md5('')), false);
  1425. }
  1426. }
  1427. private function getFileName(string $key): string
  1428. {
  1429. $s = DIRECTORY_SEPARATOR;
  1430. $md5 = md5($key);
  1431. $filename = rtrim($this->path, $s) . $s;
  1432. $i = 0;
  1433. foreach ($this->segments as $segment) {
  1434. $filename .= substr($md5, $i, $segment) . $s;
  1435. $i += $segment;
  1436. }
  1437. $filename .= substr($md5, $i);
  1438. return $filename;
  1439. }
  1440. public function set(string $key, string $value, int $ttl = 0): bool
  1441. {
  1442. $filename = $this->getFileName($key);
  1443. $dirname = dirname($filename);
  1444. if (!file_exists($dirname)) {
  1445. if (!mkdir($dirname, 0755, true)) {
  1446. return false;
  1447. }
  1448. }
  1449. $string = $ttl . '|' . $value;
  1450. return $this->filePutContents($filename, $string) !== false;
  1451. }
  1452. private function filePutContents($filename, $string)
  1453. {
  1454. return file_put_contents($filename, $string, LOCK_EX);
  1455. }
  1456. private function fileGetContents($filename)
  1457. {
  1458. $file = fopen($filename, 'rb');
  1459. if ($file === false) {
  1460. return false;
  1461. }
  1462. $lock = flock($file, LOCK_SH);
  1463. if (!$lock) {
  1464. fclose($file);
  1465. return false;
  1466. }
  1467. $string = '';
  1468. while (!feof($file)) {
  1469. $string .= fread($file, 8192);
  1470. }
  1471. flock($file, LOCK_UN);
  1472. fclose($file);
  1473. return $string;
  1474. }
  1475. private function getString($filename): string
  1476. {
  1477. $data = $this->fileGetContents($filename);
  1478. if ($data === false) {
  1479. return '';
  1480. }
  1481. list($ttl, $string) = explode('|', $data, 2);
  1482. if ($ttl > 0 && time() - filemtime($filename) > $ttl) {
  1483. return '';
  1484. }
  1485. return $string;
  1486. }
  1487. public function get(string $key): string
  1488. {
  1489. $filename = $this->getFileName($key);
  1490. if (!file_exists($filename)) {
  1491. return '';
  1492. }
  1493. $string = $this->getString($filename);
  1494. if ($string == null) {
  1495. return '';
  1496. }
  1497. return $string;
  1498. }
  1499. private function clean(string $path, array $segments, int $len, bool $all) /*: void*/
  1500. {
  1501. $entries = scandir($path);
  1502. foreach ($entries as $entry) {
  1503. if ($entry === '.' || $entry === '..') {
  1504. continue;
  1505. }
  1506. $filename = $path . DIRECTORY_SEPARATOR . $entry;
  1507. if (count($segments) == 0) {
  1508. if (strlen($entry) != $len) {
  1509. continue;
  1510. }
  1511. if (is_file($filename)) {
  1512. if ($all || $this->getString($filename) == null) {
  1513. unlink($filename);
  1514. }
  1515. }
  1516. } else {
  1517. if (strlen($entry) != $segments[0]) {
  1518. continue;
  1519. }
  1520. if (is_dir($filename)) {
  1521. $this->clean($filename, array_slice($segments, 1), $len - $segments[0], $all);
  1522. rmdir($filename);
  1523. }
  1524. }
  1525. }
  1526. }
  1527. public function clear(): bool
  1528. {
  1529. if (!file_exists($this->path) || !is_dir($this->path)) {
  1530. return false;
  1531. }
  1532. $this->clean($this->path, array_filter($this->segments), strlen(md5('')), true);
  1533. return true;
  1534. }
  1535. }
  1536. // file: src/Tqdev/PhpCrudApi/Column/Reflection/ReflectedColumn.php
  1537. class ReflectedColumn implements \JsonSerializable
  1538. {
  1539. const DEFAULT_LENGTH = 255;
  1540. const DEFAULT_PRECISION = 19;
  1541. const DEFAULT_SCALE = 4;
  1542. private $name;
  1543. private $type;
  1544. private $length;
  1545. private $precision;
  1546. private $scale;
  1547. private $nullable;
  1548. private $pk;
  1549. private $fk;
  1550. public function __construct(string $name, string $type, int $length, int $precision, int $scale, bool $nullable, bool $pk, string $fk)
  1551. {
  1552. $this->name = $name;
  1553. $this->type = $type;
  1554. $this->length = $length;
  1555. $this->precision = $precision;
  1556. $this->scale = $scale;
  1557. $this->nullable = $nullable;
  1558. $this->pk = $pk;
  1559. $this->fk = $fk;
  1560. $this->sanitize();
  1561. }
  1562. public static function fromReflection(GenericReflection $reflection, array $columnResult): ReflectedColumn
  1563. {
  1564. $name = $columnResult['COLUMN_NAME'];
  1565. $length = (int) $columnResult['CHARACTER_MAXIMUM_LENGTH'];
  1566. $type = $reflection->toJdbcType($columnResult['DATA_TYPE'], $length);
  1567. $precision = (int) $columnResult['NUMERIC_PRECISION'];
  1568. $scale = (int) $columnResult['NUMERIC_SCALE'];
  1569. $nullable = in_array(strtoupper($columnResult['IS_NULLABLE']), ['TRUE', 'YES', 'T', 'Y', '1']);
  1570. $pk = false;
  1571. $fk = '';
  1572. return new ReflectedColumn($name, $type, $length, $precision, $scale, $nullable, $pk, $fk);
  1573. }
  1574. public static function fromJson( /* object */$json): ReflectedColumn
  1575. {
  1576. $name = $json->name;
  1577. $type = $json->type;
  1578. $length = isset($json->length) ? $json->length : 0;
  1579. $precision = isset($json->precision) ? $json->precision : 0;
  1580. $scale = isset($json->scale) ? $json->scale : 0;
  1581. $nullable = isset($json->nullable) ? $json->nullable : false;
  1582. $pk = isset($json->pk) ? $json->pk : false;
  1583. $fk = isset($json->fk) ? $json->fk : '';
  1584. return new ReflectedColumn($name, $type, $length, $precision, $scale, $nullable, $pk, $fk);
  1585. }
  1586. private function sanitize()
  1587. {
  1588. $this->length = $this->hasLength() ? $this->getLength() : 0;
  1589. $this->precision = $this->hasPrecision() ? $this->getPrecision() : 0;
  1590. $this->scale = $this->hasScale() ? $this->getScale() : 0;
  1591. }
  1592. public function getName(): string
  1593. {
  1594. return $this->name;
  1595. }
  1596. public function getNullable(): bool
  1597. {
  1598. return $this->nullable;
  1599. }
  1600. public function getType(): string
  1601. {
  1602. return $this->type;
  1603. }
  1604. public function getLength(): int
  1605. {
  1606. return $this->length ?: self::DEFAULT_LENGTH;
  1607. }
  1608. public function getPrecision(): int
  1609. {
  1610. return $this->precision ?: self::DEFAULT_PRECISION;
  1611. }
  1612. public function getScale(): int
  1613. {
  1614. return $this->scale ?: self::DEFAULT_SCALE;
  1615. }
  1616. public function hasLength(): bool
  1617. {
  1618. return in_array($this->type, ['varchar', 'varbinary']);
  1619. }
  1620. public function hasPrecision(): bool
  1621. {
  1622. return $this->type == 'decimal';
  1623. }
  1624. public function hasScale(): bool
  1625. {
  1626. return $this->type == 'decimal';
  1627. }
  1628. public function isBinary(): bool
  1629. {
  1630. return in_array($this->type, ['blob', 'varbinary']);
  1631. }
  1632. public function isBoolean(): bool
  1633. {
  1634. return $this->type == 'boolean';
  1635. }
  1636. public function isGeometry(): bool
  1637. {
  1638. return $this->type == 'geometry';
  1639. }
  1640. public function isInteger(): bool
  1641. {
  1642. return in_array($this->type, ['integer', 'bigint', 'smallint', 'tinyint']);
  1643. }
  1644. public function setPk($value) /*: void*/
  1645. {
  1646. $this->pk = $value;
  1647. }
  1648. public function getPk(): bool
  1649. {
  1650. return $this->pk;
  1651. }
  1652. public function setFk($value) /*: void*/
  1653. {
  1654. $this->fk = $value;
  1655. }
  1656. public function getFk(): string
  1657. {
  1658. return $this->fk;
  1659. }
  1660. public function serialize()
  1661. {
  1662. return [
  1663. 'name' => $this->name,
  1664. 'type' => $this->type,
  1665. 'length' => $this->length,
  1666. 'precision' => $this->precision,
  1667. 'scale' => $this->scale,
  1668. 'nullable' => $this->nullable,
  1669. 'pk' => $this->pk,
  1670. 'fk' => $this->fk,
  1671. ];
  1672. }
  1673. public function jsonSerialize()
  1674. {
  1675. return array_filter($this->serialize());
  1676. }
  1677. }
  1678. // file: src/Tqdev/PhpCrudApi/Column/Reflection/ReflectedDatabase.php
  1679. class ReflectedDatabase implements \JsonSerializable
  1680. {
  1681. private $tableTypes;
  1682. public function __construct(array $tableTypes)
  1683. {
  1684. $this->tableTypes = $tableTypes;
  1685. }
  1686. public static function fromReflection(GenericReflection $reflection): ReflectedDatabase
  1687. {
  1688. $tableTypes = [];
  1689. foreach ($reflection->getTables() as $table) {
  1690. $tableName = $table['TABLE_NAME'];
  1691. $tableType = $table['TABLE_TYPE'];
  1692. if (in_array($tableName, $reflection->getIgnoredTables())) {
  1693. continue;
  1694. }
  1695. $tableTypes[$tableName] = $tableType;
  1696. }
  1697. return new ReflectedDatabase($tableTypes);
  1698. }
  1699. public static function fromJson( /* object */$json): ReflectedDatabase
  1700. {
  1701. $tableTypes = (array) $json->tables;
  1702. return new ReflectedDatabase($tableTypes);
  1703. }
  1704. public function hasTable(string $tableName): bool
  1705. {
  1706. return isset($this->tableTypes[$tableName]);
  1707. }
  1708. public function getType(string $tableName): string
  1709. {
  1710. return isset($this->tableTypes[$tableName]) ? $this->tableTypes[$tableName] : '';
  1711. }
  1712. public function getTableNames(): array
  1713. {
  1714. return array_keys($this->tableTypes);
  1715. }
  1716. public function removeTable(string $tableName): bool
  1717. {
  1718. if (!isset($this->tableTypes[$tableName])) {
  1719. return false;
  1720. }
  1721. unset($this->tableTypes[$tableName]);
  1722. return true;
  1723. }
  1724. public function serialize()
  1725. {
  1726. return [
  1727. 'tables' => $this->tableTypes,
  1728. ];
  1729. }
  1730. public function jsonSerialize()
  1731. {
  1732. return $this->serialize();
  1733. }
  1734. }
  1735. // file: src/Tqdev/PhpCrudApi/Column/Reflection/ReflectedTable.php
  1736. class ReflectedTable implements \JsonSerializable
  1737. {
  1738. private $name;
  1739. private $type;
  1740. private $columns;
  1741. private $pk;
  1742. private $fks;
  1743. public function __construct(string $name, string $type, array $columns)
  1744. {
  1745. $this->name = $name;
  1746. $this->type = $type;
  1747. $this->columns = [];
  1748. foreach ($columns as $column) {
  1749. $columnName = $column->getName();
  1750. $this->columns[$columnName] = $column;
  1751. }
  1752. $this->pk = null;
  1753. foreach ($columns as $column) {
  1754. if ($column->getPk() == true) {
  1755. $this->pk = $column;
  1756. }
  1757. }
  1758. $this->fks = [];
  1759. foreach ($columns as $column) {
  1760. $columnName = $column->getName();
  1761. $referencedTableName = $column->getFk();
  1762. if ($referencedTableName != '') {
  1763. $this->fks[$columnName] = $referencedTableName;
  1764. }
  1765. }
  1766. }
  1767. public static function fromReflection(GenericReflection $reflection, string $name, string $type): ReflectedTable
  1768. {
  1769. $columns = [];
  1770. foreach ($reflection->getTableColumns($name, $type) as $tableColumn) {
  1771. $column = ReflectedColumn::fromReflection($reflection, $tableColumn);
  1772. $columns[$column->getName()] = $column;
  1773. }
  1774. $columnNames = $reflection->getTablePrimaryKeys($name);
  1775. if (count($columnNames) == 1) {
  1776. $columnName = $columnNames[0];
  1777. if (isset($columns[$columnName])) {
  1778. $pk = $columns[$columnName];
  1779. $pk->setPk(true);
  1780. }
  1781. }
  1782. $fks = $reflection->getTableForeignKeys($name);
  1783. foreach ($fks as $columnName => $table) {
  1784. $columns[$columnName]->setFk($table);
  1785. }
  1786. return new ReflectedTable($name, $type, array_values($columns));
  1787. }
  1788. public static function fromJson( /* object */$json): ReflectedTable
  1789. {
  1790. $name = $json->name;
  1791. $type = $json->type;
  1792. $columns = [];
  1793. if (isset($json->columns) && is_array($json->columns)) {
  1794. foreach ($json->columns as $column) {
  1795. $columns[] = ReflectedColumn::fromJson($column);
  1796. }
  1797. }
  1798. return new ReflectedTable($name, $type, $columns);
  1799. }
  1800. public function hasColumn(string $columnName): bool
  1801. {
  1802. return isset($this->columns[$columnName]);
  1803. }
  1804. public function hasPk(): bool
  1805. {
  1806. return $this->pk != null;
  1807. }
  1808. public function getPk() /*: ?ReflectedColumn */
  1809. {
  1810. return $this->pk;
  1811. }
  1812. public function getName(): string
  1813. {
  1814. return $this->name;
  1815. }
  1816. public function getType(): string
  1817. {
  1818. return $this->type;
  1819. }
  1820. public function getColumnNames(): array
  1821. {
  1822. return array_keys($this->columns);
  1823. }
  1824. public function getColumn($columnName): ReflectedColumn
  1825. {
  1826. return $this->columns[$columnName];
  1827. }
  1828. public function getFksTo(string $tableName): array
  1829. {
  1830. $columns = array();
  1831. foreach ($this->fks as $columnName => $referencedTableName) {
  1832. if ($tableName == $referencedTableName) {
  1833. $columns[] = $this->columns[$columnName];
  1834. }
  1835. }
  1836. return $columns;
  1837. }
  1838. public function removeColumn(string $columnName): bool
  1839. {
  1840. if (!isset($this->columns[$columnName])) {
  1841. return false;
  1842. }
  1843. unset($this->columns[$columnName]);
  1844. return true;
  1845. }
  1846. public function serialize()
  1847. {
  1848. return [
  1849. 'name' => $this->name,
  1850. 'type' => $this->type,
  1851. 'columns' => array_values($this->columns),
  1852. ];
  1853. }
  1854. public function jsonSerialize()
  1855. {
  1856. return $this->serialize();
  1857. }
  1858. }
  1859. // file: src/Tqdev/PhpCrudApi/Column/DefinitionService.php
  1860. class DefinitionService
  1861. {
  1862. private $db;
  1863. private $reflection;
  1864. public function __construct(GenericDB $db, ReflectionService $reflection)
  1865. {
  1866. $this->db = $db;
  1867. $this->reflection = $reflection;
  1868. }
  1869. public function updateTable(string $tableName, /* object */ $changes): bool
  1870. {
  1871. $table = $this->reflection->getTable($tableName);
  1872. $newTable = ReflectedTable::fromJson((object) array_merge((array) $table->jsonSerialize(), (array) $changes));
  1873. if ($table->getName() != $newTable->getName()) {
  1874. if (!$this->db->definition()->renameTable($table->getName(), $newTable->getName())) {
  1875. return false;
  1876. }
  1877. }
  1878. return true;
  1879. }
  1880. public function updateColumn(string $tableName, string $columnName, /* object */ $changes): bool
  1881. {
  1882. $table = $this->reflection->getTable($tableName);
  1883. $column = $table->getColumn($columnName);
  1884. $newColumn = ReflectedColumn::fromJson((object) array_merge((array) $column->jsonSerialize(), (array) $changes));
  1885. if ($newColumn->getPk() != $column->getPk() && $table->hasPk()) {
  1886. $oldColumn = $table->getPk();
  1887. if ($oldColumn->getName() != $columnName) {
  1888. $oldColumn->setPk(false);
  1889. if (!$this->db->definition()->removeColumnPrimaryKey($table->getName(), $oldColumn->getName(), $oldColumn)) {
  1890. return false;
  1891. }
  1892. }
  1893. }
  1894. $newColumn = ReflectedColumn::fromJson((object) array_merge((array) $column->jsonSerialize(), ['pk' => false, 'fk' => false]));
  1895. if ($newColumn->getPk() != $column->getPk() && !$newColumn->getPk()) {
  1896. if (!$this->db->definition()->removeColumnPrimaryKey($table->getName(), $column->getName(), $newColumn)) {
  1897. return false;
  1898. }
  1899. }
  1900. if ($newColumn->getFk() != $column->getFk() && !$newColumn->getFk()) {
  1901. if (!$this->db->definition()->removeColumnForeignKey($table->getName(), $column->getName(), $newColumn)) {
  1902. return false;
  1903. }
  1904. }
  1905. $newColumn = ReflectedColumn::fromJson((object) array_merge((array) $column->jsonSerialize(), (array) $changes));
  1906. $newColumn->setPk(false);
  1907. $newColumn->setFk('');
  1908. if ($newColumn->getName() != $column->getName()) {
  1909. if (!$this->db->definition()->renameColumn($table->getName(), $column->getName(), $newColumn)) {
  1910. return false;
  1911. }
  1912. }
  1913. if ($newColumn->getType() != $column->getType() ||
  1914. $newColumn->getLength() != $column->getLength() ||
  1915. $newColumn->getPrecision() != $column->getPrecision() ||
  1916. $newColumn->getScale() != $column->getScale()
  1917. ) {
  1918. if (!$this->db->definition()->retypeColumn($table->getName(), $newColumn->getName(), $newColumn)) {
  1919. return false;
  1920. }
  1921. }
  1922. if ($newColumn->getNullable() != $column->getNullable()) {
  1923. if (!$this->db->definition()->setColumnNullable($table->getName(), $newColumn->getName(), $newColumn)) {
  1924. return false;
  1925. }
  1926. }
  1927. $newColumn = ReflectedColumn::fromJson((object) array_merge((array) $column->jsonSerialize(), (array) $changes));
  1928. if ($newColumn->getFk()) {
  1929. if (!$this->db->definition()->addColumnForeignKey($table->getName(), $newColumn->getName(), $newColumn)) {
  1930. return false;
  1931. }
  1932. }
  1933. if ($newColumn->getPk()) {
  1934. if (!$this->db->definition()->addColumnPrimaryKey($table->getName(), $newColumn->getName(), $newColumn)) {
  1935. return false;
  1936. }
  1937. }
  1938. return true;
  1939. }
  1940. public function addTable( /* object */$definition)
  1941. {
  1942. $newTable = ReflectedTable::fromJson($definition);
  1943. if (!$this->db->definition()->addTable($newTable)) {
  1944. return false;
  1945. }
  1946. return true;
  1947. }
  1948. public function addColumn(string $tableName, /* object */ $definition)
  1949. {
  1950. $newColumn = ReflectedColumn::fromJson($definition);
  1951. if (!$this->db->definition()->addColumn($tableName, $newColumn)) {
  1952. return false;
  1953. }
  1954. if ($newColumn->getFk()) {
  1955. if (!$this->db->definition()->addColumnForeignKey($tableName, $newColumn->getName(), $newColumn)) {
  1956. return false;
  1957. }
  1958. }
  1959. if ($newColumn->getPk()) {
  1960. if (!$this->db->definition()->addColumnPrimaryKey($tableName, $newColumn->getName(), $newColumn)) {
  1961. return false;
  1962. }
  1963. }
  1964. return true;
  1965. }
  1966. public function removeTable(string $tableName)
  1967. {
  1968. if (!$this->db->definition()->removeTable($tableName)) {
  1969. return false;
  1970. }
  1971. return true;
  1972. }
  1973. public function removeColumn(string $tableName, string $columnName)
  1974. {
  1975. $table = $this->reflection->getTable($tableName);
  1976. $newColumn = $table->getColumn($columnName);
  1977. if ($newColumn->getPk()) {
  1978. $newColumn->setPk(false);
  1979. if (!$this->db->definition()->removeColumnPrimaryKey($table->getName(), $newColumn->getName(), $newColumn)) {
  1980. return false;
  1981. }
  1982. }
  1983. if ($newColumn->getFk()) {
  1984. $newColumn->setFk("");
  1985. if (!$this->db->definition()->removeColumnForeignKey($tableName, $columnName, $newColumn)) {
  1986. return false;
  1987. }
  1988. }
  1989. if (!$this->db->definition()->removeColumn($tableName, $columnName)) {
  1990. return false;
  1991. }
  1992. return true;
  1993. }
  1994. }
  1995. // file: src/Tqdev/PhpCrudApi/Column/ReflectionService.php
  1996. class ReflectionService
  1997. {
  1998. private $db;
  1999. private $cache;
  2000. private $ttl;
  2001. private $database;
  2002. private $tables;
  2003. public function __construct(GenericDB $db, Cache $cache, int $ttl)
  2004. {
  2005. $this->db = $db;
  2006. $this->cache = $cache;
  2007. $this->ttl = $ttl;
  2008. $this->database = $this->loadDatabase(true);
  2009. $this->tables = [];
  2010. }
  2011. private function loadDatabase(bool $useCache): ReflectedDatabase
  2012. {
  2013. $data = $useCache ? $this->cache->get('ReflectedDatabase') : '';
  2014. if ($data != '') {
  2015. $database = ReflectedDatabase::fromJson(json_decode(gzuncompress($data)));
  2016. } else {
  2017. $database = ReflectedDatabase::fromReflection($this->db->reflection());
  2018. $data = gzcompress(json_encode($database, JSON_UNESCAPED_UNICODE));
  2019. $this->cache->set('ReflectedDatabase', $data, $this->ttl);
  2020. }
  2021. return $database;
  2022. }
  2023. private function loadTable(string $tableName, bool $useCache): ReflectedTable
  2024. {
  2025. $data = $useCache ? $this->cache->get("ReflectedTable($tableName)") : '';
  2026. if ($data != '') {
  2027. $table = ReflectedTable::fromJson(json_decode(gzuncompress($data)));
  2028. } else {
  2029. $tableType = $this->database->getType($tableName);
  2030. $table = ReflectedTable::fromReflection($this->db->reflection(), $tableName, $tableType);
  2031. $data = gzcompress(json_encode($table, JSON_UNESCAPED_UNICODE));
  2032. $this->cache->set("ReflectedTable($tableName)", $data, $this->ttl);
  2033. }
  2034. return $table;
  2035. }
  2036. public function refreshTables()
  2037. {
  2038. $this->database = $this->loadDatabase(false);
  2039. }
  2040. public function refreshTable(string $tableName)
  2041. {
  2042. $this->tables[$tableName] = $this->loadTable($tableName, false);
  2043. }
  2044. public function hasTable(string $tableName): bool
  2045. {
  2046. return $this->database->hasTable($tableName);
  2047. }
  2048. public function getType(string $tableName): string
  2049. {
  2050. return $this->database->getType($tableName);
  2051. }
  2052. public function getTable(string $tableName): ReflectedTable
  2053. {
  2054. if (!isset($this->tables[$tableName])) {
  2055. $this->tables[$tableName] = $this->loadTable($tableName, true);
  2056. }
  2057. return $this->tables[$tableName];
  2058. }
  2059. public function getTableNames(): array
  2060. {
  2061. return $this->database->getTableNames();
  2062. }
  2063. public function getDatabaseName(): string
  2064. {
  2065. return $this->database->getName();
  2066. }
  2067. public function removeTable(string $tableName): bool
  2068. {
  2069. unset($this->tables[$tableName]);
  2070. return $this->database->removeTable($tableName);
  2071. }
  2072. }
  2073. // file: src/Tqdev/PhpCrudApi/Controller/CacheController.php
  2074. class CacheController
  2075. {
  2076. private $cache;
  2077. private $responder;
  2078. public function __construct(Router $router, Responder $responder, Cache $cache)
  2079. {
  2080. $router->register('GET', '/cache/clear', array($this, 'clear'));
  2081. $this->cache = $cache;
  2082. $this->responder = $responder;
  2083. }
  2084. public function clear(ServerRequestInterface $request): ResponseInterface
  2085. {
  2086. return $this->responder->success($this->cache->clear());
  2087. }
  2088. }
  2089. // file: src/Tqdev/PhpCrudApi/Controller/ColumnController.php
  2090. class ColumnController
  2091. {
  2092. private $responder;
  2093. private $reflection;
  2094. private $definition;
  2095. public function __construct(Router $router, Responder $responder, ReflectionService $reflection, DefinitionService $definition)
  2096. {
  2097. $router->register('GET', '/columns', array($this, 'getDatabase'));
  2098. $router->register('GET', '/columns/*', array($this, 'getTable'));
  2099. $router->register('GET', '/columns/*/*', array($this, 'getColumn'));
  2100. $router->register('PUT', '/columns/*', array($this, 'updateTable'));
  2101. $router->register('PUT', '/columns/*/*', array($this, 'updateColumn'));
  2102. $router->register('POST', '/columns', array($this, 'addTable'));
  2103. $router->register('POST', '/columns/*', array($this, 'addColumn'));
  2104. $router->register('DELETE', '/columns/*', array($this, 'removeTable'));
  2105. $router->register('DELETE', '/columns/*/*', array($this, 'removeColumn'));
  2106. $this->responder = $responder;
  2107. $this->reflection = $reflection;
  2108. $this->definition = $definition;
  2109. }
  2110. public function getDatabase(ServerRequestInterface $request): ResponseInterface
  2111. {
  2112. $tables = [];
  2113. foreach ($this->reflection->getTableNames() as $table) {
  2114. $tables[] = $this->reflection->getTable($table);
  2115. }
  2116. $database = ['tables' => $tables];
  2117. return $this->responder->success($database);
  2118. }
  2119. public function getTable(ServerRequestInterface $request): ResponseInterface
  2120. {
  2121. $tableName = RequestUtils::getPathSegment($request, 2);
  2122. if (!$this->reflection->hasTable($tableName)) {
  2123. return $this->responder->error(ErrorCode::TABLE_NOT_FOUND, $tableName);
  2124. }
  2125. $table = $this->reflection->getTable($tableName);
  2126. return $this->responder->success($table);
  2127. }
  2128. public function getColumn(ServerRequestInterface $request): ResponseInterface
  2129. {
  2130. $tableName = RequestUtils::getPathSegment($request, 2);
  2131. $columnName = RequestUtils::getPathSegment($request, 3);
  2132. if (!$this->reflection->hasTable($tableName)) {
  2133. return $this->responder->error(ErrorCode::TABLE_NOT_FOUND, $tableName);
  2134. }
  2135. $table = $this->reflection->getTable($tableName);
  2136. if (!$table->hasColumn($columnName)) {
  2137. return $this->responder->error(ErrorCode::COLUMN_NOT_FOUND, $columnName);
  2138. }
  2139. $column = $table->getColumn($columnName);
  2140. return $this->responder->success($column);
  2141. }
  2142. public function updateTable(ServerRequestInterface $request): ResponseInterface
  2143. {
  2144. $tableName = RequestUtils::getPathSegment($request, 2);
  2145. if (!$this->reflection->hasTable($tableName)) {
  2146. return $this->responder->error(ErrorCode::TABLE_NOT_FOUND, $tableName);
  2147. }
  2148. $success = $this->definition->updateTable($tableName, $request->getParsedBody());
  2149. if ($success) {
  2150. $this->reflection->refreshTables();
  2151. }
  2152. return $this->responder->success($success);
  2153. }
  2154. public function updateColumn(ServerRequestInterface $request): ResponseInterface
  2155. {
  2156. $tableName = RequestUtils::getPathSegment($request, 2);
  2157. $columnName = RequestUtils::getPathSegment($request, 3);
  2158. if (!$this->reflection->hasTable($tableName)) {
  2159. return $this->responder->error(ErrorCode::TABLE_NOT_FOUND, $tableName);
  2160. }
  2161. $table = $this->reflection->getTable($tableName);
  2162. if (!$table->hasColumn($columnName)) {
  2163. return $this->responder->error(ErrorCode::COLUMN_NOT_FOUND, $columnName);
  2164. }
  2165. $success = $this->definition->updateColumn($tableName, $columnName, $request->getParsedBody());
  2166. if ($success) {
  2167. $this->reflection->refreshTable($tableName);
  2168. }
  2169. return $this->responder->success($success);
  2170. }
  2171. public function addTable(ServerRequestInterface $request): ResponseInterface
  2172. {
  2173. $tableName = $request->getParsedBody()->name;
  2174. if ($this->reflection->hasTable($tableName)) {
  2175. return $this->responder->error(ErrorCode::TABLE_ALREADY_EXISTS, $tableName);
  2176. }
  2177. $success = $this->definition->addTable($request->getParsedBody());
  2178. if ($success) {
  2179. $this->reflection->refreshTables();
  2180. }
  2181. return $this->responder->success($success);
  2182. }
  2183. public function addColumn(ServerRequestInterface $request): ResponseInterface
  2184. {
  2185. $tableName = RequestUtils::getPathSegment($request, 2);
  2186. if (!$this->reflection->hasTable($tableName)) {
  2187. return $this->responder->error(ErrorCode::TABLE_NOT_FOUND, $tableName);
  2188. }
  2189. $columnName = $request->getParsedBody()->name;
  2190. $table = $this->reflection->getTable($tableName);
  2191. if ($table->hasColumn($columnName)) {
  2192. return $this->responder->error(ErrorCode::COLUMN_ALREADY_EXISTS, $columnName);
  2193. }
  2194. $success = $this->definition->addColumn($tableName, $request->getParsedBody());
  2195. if ($success) {
  2196. $this->reflection->refreshTable($tableName);
  2197. }
  2198. return $this->responder->success($success);
  2199. }
  2200. public function removeTable(ServerRequestInterface $request): ResponseInterface
  2201. {
  2202. $tableName = RequestUtils::getPathSegment($request, 2);
  2203. if (!$this->reflection->hasTable($tableName)) {
  2204. return $this->responder->error(ErrorCode::TABLE_NOT_FOUND, $tableName);
  2205. }
  2206. $success = $this->definition->removeTable($tableName);
  2207. if ($success) {
  2208. $this->reflection->refreshTables();
  2209. }
  2210. return $this->responder->success($success);
  2211. }
  2212. public function removeColumn(ServerRequestInterface $request): ResponseInterface
  2213. {
  2214. $tableName = RequestUtils::getPathSegment($request, 2);
  2215. $columnName = RequestUtils::getPathSegment($request, 3);
  2216. if (!$this->reflection->hasTable($tableName)) {
  2217. return $this->responder->error(ErrorCode::TABLE_NOT_FOUND, $tableName);
  2218. }
  2219. $table = $this->reflection->getTable($tableName);
  2220. if (!$table->hasColumn($columnName)) {
  2221. return $this->responder->error(ErrorCode::COLUMN_NOT_FOUND, $columnName);
  2222. }
  2223. $success = $this->definition->removeColumn($tableName, $columnName);
  2224. if ($success) {
  2225. $this->reflection->refreshTable($tableName);
  2226. }
  2227. return $this->responder->success($success);
  2228. }
  2229. }
  2230. // file: src/Tqdev/PhpCrudApi/Controller/OpenApiController.php
  2231. class OpenApiController
  2232. {
  2233. private $openApi;
  2234. private $responder;
  2235. public function __construct(Router $router, Responder $responder, OpenApiService $openApi)
  2236. {
  2237. $router->register('GET', '/openapi', array($this, 'openapi'));
  2238. $this->openApi = $openApi;
  2239. $this->responder = $responder;
  2240. }
  2241. public function openapi(ServerRequestInterface $request): ResponseInterface
  2242. {
  2243. return $this->responder->success($this->openApi->get());
  2244. }
  2245. }
  2246. // file: src/Tqdev/PhpCrudApi/Controller/RecordController.php
  2247. class RecordController
  2248. {
  2249. private $service;
  2250. private $responder;
  2251. public function __construct(Router $router, Responder $responder, RecordService $service)
  2252. {
  2253. $router->register('GET', '/records/*', array($this, '_list'));
  2254. $router->register('POST', '/records/*', array($this, 'create'));
  2255. $router->register('GET', '/records/*/*', array($this, 'read'));
  2256. $router->register('PUT', '/records/*/*', array($this, 'update'));
  2257. $router->register('DELETE', '/records/*/*', array($this, 'delete'));
  2258. $router->register('PATCH', '/records/*/*', array($this, 'increment'));
  2259. $this->service = $service;
  2260. $this->responder = $responder;
  2261. }
  2262. public function _list(ServerRequestInterface $request): ResponseInterface
  2263. {
  2264. $table = RequestUtils::getPathSegment($request, 2);
  2265. $params = RequestUtils::getParams($request);
  2266. if (!$this->service->hasTable($table)) {
  2267. return $this->responder->error(ErrorCode::TABLE_NOT_FOUND, $table);
  2268. }
  2269. return $this->responder->success($this->service->_list($table, $params));
  2270. }
  2271. public function read(ServerRequestInterface $request): ResponseInterface
  2272. {
  2273. $table = RequestUtils::getPathSegment($request, 2);
  2274. if (!$this->service->hasTable($table)) {
  2275. return $this->responder->error(ErrorCode::TABLE_NOT_FOUND, $table);
  2276. }
  2277. if ($this->service->getType($table) != 'table') {
  2278. return $this->responder->error(ErrorCode::OPERATION_NOT_SUPPORTED, __FUNCTION__);
  2279. }
  2280. $id = RequestUtils::getPathSegment($request, 3);
  2281. $params = RequestUtils::getParams($request);
  2282. if (strpos($id, ',') !== false) {
  2283. $ids = explode(',', $id);
  2284. $result = [];
  2285. for ($i = 0; $i < count($ids); $i++) {
  2286. array_push($result, $this->service->read($table, $ids[$i], $params));
  2287. }
  2288. return $this->responder->success($result);
  2289. } else {
  2290. $response = $this->service->read($table, $id, $params);
  2291. if ($response === null) {
  2292. return $this->responder->error(ErrorCode::RECORD_NOT_FOUND, $id);
  2293. }
  2294. return $this->responder->success($response);
  2295. }
  2296. }
  2297. public function create(ServerRequestInterface $request): ResponseInterface
  2298. {
  2299. $table = RequestUtils::getPathSegment($request, 2);
  2300. if (!$this->service->hasTable($table)) {
  2301. return $this->responder->error(ErrorCode::TABLE_NOT_FOUND, $table);
  2302. }
  2303. if ($this->service->getType($table) != 'table') {
  2304. return $this->responder->error(ErrorCode::OPERATION_NOT_SUPPORTED, __FUNCTION__);
  2305. }
  2306. $record = $request->getParsedBody();
  2307. if ($record === null) {
  2308. return $this->responder->error(ErrorCode::HTTP_MESSAGE_NOT_READABLE, '');
  2309. }
  2310. $params = RequestUtils::getParams($request);
  2311. if (is_array($record)) {
  2312. $result = array();
  2313. foreach ($record as $r) {
  2314. $result[] = $this->service->create($table, $r, $params);
  2315. }
  2316. return $this->responder->success($result);
  2317. } else {
  2318. return $this->responder->success($this->service->create($table, $record, $params));
  2319. }
  2320. }
  2321. public function update(ServerRequestInterface $request): ResponseInterface
  2322. {
  2323. $table = RequestUtils::getPathSegment($request, 2);
  2324. if (!$this->service->hasTable($table)) {
  2325. return $this->responder->error(ErrorCode::TABLE_NOT_FOUND, $table);
  2326. }
  2327. if ($this->service->getType($table) != 'table') {
  2328. return $this->responder->error(ErrorCode::OPERATION_NOT_SUPPORTED, __FUNCTION__);
  2329. }
  2330. $id = RequestUtils::getPathSegment($request, 3);
  2331. $params = RequestUtils::getParams($request);
  2332. $record = $request->getParsedBody();
  2333. if ($record === null) {
  2334. return $this->responder->error(ErrorCode::HTTP_MESSAGE_NOT_READABLE, '');
  2335. }
  2336. $ids = explode(',', $id);
  2337. if (is_array($record)) {
  2338. if (count($ids) != count($record)) {
  2339. return $this->responder->error(ErrorCode::ARGUMENT_COUNT_MISMATCH, $id);
  2340. }
  2341. $result = array();
  2342. for ($i = 0; $i < count($ids); $i++) {
  2343. $result[] = $this->service->update($table, $ids[$i], $record[$i], $params);
  2344. }
  2345. return $this->responder->success($result);
  2346. } else {
  2347. if (count($ids) != 1) {
  2348. return $this->responder->error(ErrorCode::ARGUMENT_COUNT_MISMATCH, $id);
  2349. }
  2350. return $this->responder->success($this->service->update($table, $id, $record, $params));
  2351. }
  2352. }
  2353. public function delete(ServerRequestInterface $request): ResponseInterface
  2354. {
  2355. $table = RequestUtils::getPathSegment($request, 2);
  2356. if (!$this->service->hasTable($table)) {
  2357. return $this->responder->error(ErrorCode::TABLE_NOT_FOUND, $table);
  2358. }
  2359. if ($this->service->getType($table) != 'table') {
  2360. return $this->responder->error(ErrorCode::OPERATION_NOT_SUPPORTED, __FUNCTION__);
  2361. }
  2362. $id = RequestUtils::getPathSegment($request, 3);
  2363. $params = RequestUtils::getParams($request);
  2364. $ids = explode(',', $id);
  2365. if (count($ids) > 1) {
  2366. $result = array();
  2367. for ($i = 0; $i < count($ids); $i++) {
  2368. $result[] = $this->service->delete($table, $ids[$i], $params);
  2369. }
  2370. return $this->responder->success($result);
  2371. } else {
  2372. return $this->responder->success($this->service->delete($table, $id, $params));
  2373. }
  2374. }
  2375. public function increment(ServerRequestInterface $request): ResponseInterface
  2376. {
  2377. $table = RequestUtils::getPathSegment($request, 2);
  2378. if (!$this->service->hasTable($table)) {
  2379. return $this->responder->error(ErrorCode::TABLE_NOT_FOUND, $table);
  2380. }
  2381. if ($this->service->getType($table) != 'table') {
  2382. return $this->responder->error(ErrorCode::OPERATION_NOT_SUPPORTED, __FUNCTION__);
  2383. }
  2384. $id = RequestUtils::getPathSegment($request, 3);
  2385. $record = $request->getParsedBody();
  2386. if ($record === null) {
  2387. return $this->responder->error(ErrorCode::HTTP_MESSAGE_NOT_READABLE, '');
  2388. }
  2389. $params = RequestUtils::getParams($request);
  2390. $ids = explode(',', $id);
  2391. if (is_array($record)) {
  2392. if (count($ids) != count($record)) {
  2393. return $this->responder->error(ErrorCode::ARGUMENT_COUNT_MISMATCH, $id);
  2394. }
  2395. $result = array();
  2396. for ($i = 0; $i < count($ids); $i++) {
  2397. $result[] = $this->service->increment($table, $ids[$i], $record[$i], $params);
  2398. }
  2399. return $this->responder->success($result);
  2400. } else {
  2401. if (count($ids) != 1) {
  2402. return $this->responder->error(ErrorCode::ARGUMENT_COUNT_MISMATCH, $id);
  2403. }
  2404. return $this->responder->success($this->service->increment($table, $id, $record, $params));
  2405. }
  2406. }
  2407. }
  2408. // file: src/Tqdev/PhpCrudApi/Controller/Responder.php
  2409. class Responder
  2410. {
  2411. public function error(int $error, string $argument, $details = null): ResponseInterface
  2412. {
  2413. $errorCode = new ErrorCode($error);
  2414. $status = $errorCode->getStatus();
  2415. $document = new ErrorDocument($errorCode, $argument, $details);
  2416. return ResponseFactory::fromObject($status, $document);
  2417. }
  2418. public function success($result): ResponseInterface
  2419. {
  2420. return ResponseFactory::fromObject(ResponseFactory::OK, $result);
  2421. }
  2422. }
  2423. // file: src/Tqdev/PhpCrudApi/Database/ColumnConverter.php
  2424. class ColumnConverter
  2425. {
  2426. private $driver;
  2427. public function __construct(string $driver)
  2428. {
  2429. $this->driver = $driver;
  2430. }
  2431. public function convertColumnValue(ReflectedColumn $column): string
  2432. {
  2433. if ($column->isBinary()) {
  2434. switch ($this->driver) {
  2435. case 'mysql':
  2436. return "FROM_BASE64(?)";
  2437. case 'pgsql':
  2438. return "decode(?, 'base64')";
  2439. case 'sqlsrv':
  2440. return "CONVERT(XML, ?).value('.','varbinary(max)')";
  2441. }
  2442. }
  2443. if ($column->isGeometry()) {
  2444. switch ($this->driver) {
  2445. case 'mysql':
  2446. case 'pgsql':
  2447. return "ST_GeomFromText(?)";
  2448. case 'sqlsrv':
  2449. return "geometry::STGeomFromText(?,0)";
  2450. }
  2451. }
  2452. return '?';
  2453. }
  2454. public function convertColumnName(ReflectedColumn $column, $value): string
  2455. {
  2456. if ($column->isBinary()) {
  2457. switch ($this->driver) {
  2458. case 'mysql':
  2459. return "TO_BASE64($value) as $value";
  2460. case 'pgsql':
  2461. return "encode($value::bytea, 'base64') as $value";
  2462. case 'sqlsrv':
  2463. return "CASE WHEN $value IS NULL THEN NULL ELSE (SELECT CAST($value as varbinary(max)) FOR XML PATH(''), BINARY BASE64) END as $value";
  2464. }
  2465. }
  2466. if ($column->isGeometry()) {
  2467. switch ($this->driver) {
  2468. case 'mysql':
  2469. case 'pgsql':
  2470. return "ST_AsText($value) as $value";
  2471. case 'sqlsrv':
  2472. return "REPLACE($value.STAsText(),' (','(') as $value";
  2473. }
  2474. }
  2475. return $value;
  2476. }
  2477. }
  2478. // file: src/Tqdev/PhpCrudApi/Database/ColumnsBuilder.php
  2479. class ColumnsBuilder
  2480. {
  2481. private $driver;
  2482. private $converter;
  2483. public function __construct(string $driver)
  2484. {
  2485. $this->driver = $driver;
  2486. $this->converter = new ColumnConverter($driver);
  2487. }
  2488. public function getOffsetLimit(int $offset, int $limit): string
  2489. {
  2490. if ($limit < 0 || $offset < 0) {
  2491. return '';
  2492. }
  2493. switch ($this->driver) {
  2494. case 'mysql':return " LIMIT $offset, $limit";
  2495. case 'pgsql':return " LIMIT $limit OFFSET $offset";
  2496. case 'sqlsrv':return " OFFSET $offset ROWS FETCH NEXT $limit ROWS ONLY";
  2497. }
  2498. }
  2499. private function quoteColumnName(ReflectedColumn $column): string
  2500. {
  2501. return '"' . $column->getName() . '"';
  2502. }
  2503. public function getOrderBy(ReflectedTable $table, array $columnOrdering): string
  2504. {
  2505. if (count($columnOrdering) == 0) {
  2506. return '';
  2507. }
  2508. $results = array();
  2509. foreach ($columnOrdering as $i => list($columnName, $ordering)) {
  2510. $column = $table->getColumn($columnName);
  2511. $quotedColumnName = $this->quoteColumnName($column);
  2512. $results[] = $quotedColumnName . ' ' . $ordering;
  2513. }
  2514. return ' ORDER BY ' . implode(',', $results);
  2515. }
  2516. public function getSelect(ReflectedTable $table, array $columnNames): string
  2517. {
  2518. $results = array();
  2519. foreach ($columnNames as $columnName) {
  2520. $column = $table->getColumn($columnName);
  2521. $quotedColumnName = $this->quoteColumnName($column);
  2522. $quotedColumnName = $this->converter->convertColumnName($column, $quotedColumnName);
  2523. $results[] = $quotedColumnName;
  2524. }
  2525. return implode(',', $results);
  2526. }
  2527. public function getInsert(ReflectedTable $table, array $columnValues): string
  2528. {
  2529. $columns = array();
  2530. $values = array();
  2531. foreach ($columnValues as $columnName => $columnValue) {
  2532. $column = $table->getColumn($columnName);
  2533. $quotedColumnName = $this->quoteColumnName($column);
  2534. $columns[] = $quotedColumnName;
  2535. $columnValue = $this->converter->convertColumnValue($column);
  2536. $values[] = $columnValue;
  2537. }
  2538. $columnsSql = '(' . implode(',', $columns) . ')';
  2539. $valuesSql = '(' . implode(',', $values) . ')';
  2540. $outputColumn = $this->quoteColumnName($table->getPk());
  2541. switch ($this->driver) {
  2542. case 'mysql':return "$columnsSql VALUES $valuesSql";
  2543. case 'pgsql':return "$columnsSql VALUES $valuesSql RETURNING $outputColumn";
  2544. case 'sqlsrv':return "$columnsSql OUTPUT INSERTED.$outputColumn VALUES $valuesSql";
  2545. }
  2546. }
  2547. public function getUpdate(ReflectedTable $table, array $columnValues): string
  2548. {
  2549. $results = array();
  2550. foreach ($columnValues as $columnName => $columnValue) {
  2551. $column = $table->getColumn($columnName);
  2552. $quotedColumnName = $this->quoteColumnName($column);
  2553. $columnValue = $this->converter->convertColumnValue($column);
  2554. $results[] = $quotedColumnName . '=' . $columnValue;
  2555. }
  2556. return implode(',', $results);
  2557. }
  2558. public function getIncrement(ReflectedTable $table, array $columnValues): string
  2559. {
  2560. $results = array();
  2561. foreach ($columnValues as $columnName => $columnValue) {
  2562. if (!is_numeric($columnValue)) {
  2563. continue;
  2564. }
  2565. $column = $table->getColumn($columnName);
  2566. $quotedColumnName = $this->quoteColumnName($column);
  2567. $columnValue = $this->converter->convertColumnValue($column);
  2568. $results[] = $quotedColumnName . '=' . $quotedColumnName . '+' . $columnValue;
  2569. }
  2570. return implode(',', $results);
  2571. }
  2572. }
  2573. // file: src/Tqdev/PhpCrudApi/Database/ConditionsBuilder.php
  2574. class ConditionsBuilder
  2575. {
  2576. private $driver;
  2577. public function __construct(string $driver)
  2578. {
  2579. $this->driver = $driver;
  2580. }
  2581. private function getConditionSql(Condition $condition, array &$arguments): string
  2582. {
  2583. if ($condition instanceof AndCondition) {
  2584. return $this->getAndConditionSql($condition, $arguments);
  2585. }
  2586. if ($condition instanceof OrCondition) {
  2587. return $this->getOrConditionSql($condition, $arguments);
  2588. }
  2589. if ($condition instanceof NotCondition) {
  2590. return $this->getNotConditionSql($condition, $arguments);
  2591. }
  2592. if ($condition instanceof SpatialCondition) {
  2593. return $this->getSpatialConditionSql($condition, $arguments);
  2594. }
  2595. if ($condition instanceof ColumnCondition) {
  2596. return $this->getColumnConditionSql($condition, $arguments);
  2597. }
  2598. throw new \Exception('Unknown Condition: ' . get_class($condition));
  2599. }
  2600. private function getAndConditionSql(AndCondition $and, array &$arguments): string
  2601. {
  2602. $parts = [];
  2603. foreach ($and->getConditions() as $condition) {
  2604. $parts[] = $this->getConditionSql($condition, $arguments);
  2605. }
  2606. return '(' . implode(' AND ', $parts) . ')';
  2607. }
  2608. private function getOrConditionSql(OrCondition $or, array &$arguments): string
  2609. {
  2610. $parts = [];
  2611. foreach ($or->getConditions() as $condition) {
  2612. $parts[] = $this->getConditionSql($condition, $arguments);
  2613. }
  2614. return '(' . implode(' OR ', $parts) . ')';
  2615. }
  2616. private function getNotConditionSql(NotCondition $not, array &$arguments): string
  2617. {
  2618. $condition = $not->getCondition();
  2619. return '(NOT ' . $this->getConditionSql($condition, $arguments) . ')';
  2620. }
  2621. private function quoteColumnName(ReflectedColumn $column): string
  2622. {
  2623. return '"' . $column->getName() . '"';
  2624. }
  2625. private function escapeLikeValue(string $value): string
  2626. {
  2627. return addcslashes($value, '%_');
  2628. }
  2629. private function getColumnConditionSql(ColumnCondition $condition, array &$arguments): string
  2630. {
  2631. $column = $this->quoteColumnName($condition->getColumn());
  2632. $operator = $condition->getOperator();
  2633. $value = $condition->getValue();
  2634. switch ($operator) {
  2635. case 'cs':
  2636. $sql = "$column LIKE ?";
  2637. $arguments[] = '%' . $this->escapeLikeValue($value) . '%';
  2638. break;
  2639. case 'sw':
  2640. $sql = "$column LIKE ?";
  2641. $arguments[] = $this->escapeLikeValue($value) . '%';
  2642. break;
  2643. case 'ew':
  2644. $sql = "$column LIKE ?";
  2645. $arguments[] = '%' . $this->escapeLikeValue($value);
  2646. break;
  2647. case 'eq':
  2648. $sql = "$column = ?";
  2649. $arguments[] = $value;
  2650. break;
  2651. case 'lt':
  2652. $sql = "$column < ?";
  2653. $arguments[] = $value;
  2654. break;
  2655. case 'le':
  2656. $sql = "$column <= ?";
  2657. $arguments[] = $value;
  2658. break;
  2659. case 'ge':
  2660. $sql = "$column >= ?";
  2661. $arguments[] = $value;
  2662. break;
  2663. case 'gt':
  2664. $sql = "$column > ?";
  2665. $arguments[] = $value;
  2666. break;
  2667. case 'bt':
  2668. $parts = explode(',', $value, 2);
  2669. $count = count($parts);
  2670. if ($count == 2) {
  2671. $sql = "($column >= ? AND $column <= ?)";
  2672. $arguments[] = $parts[0];
  2673. $arguments[] = $parts[1];
  2674. } else {
  2675. $sql = "FALSE";
  2676. }
  2677. break;
  2678. case 'in':
  2679. $parts = explode(',', $value);
  2680. $count = count($parts);
  2681. if ($count > 0) {
  2682. $qmarks = implode(',', str_split(str_repeat('?', $count)));
  2683. $sql = "$column IN ($qmarks)";
  2684. for ($i = 0; $i < $count; $i++) {
  2685. $arguments[] = $parts[$i];
  2686. }
  2687. } else {
  2688. $sql = "FALSE";
  2689. }
  2690. break;
  2691. case 'is':
  2692. $sql = "$column IS NULL";
  2693. break;
  2694. }
  2695. return $sql;
  2696. }
  2697. private function getSpatialFunctionName(string $operator): string
  2698. {
  2699. switch ($operator) {
  2700. case 'co':return 'ST_Contains';
  2701. case 'cr':return 'ST_Crosses';
  2702. case 'di':return 'ST_Disjoint';
  2703. case 'eq':return 'ST_Equals';
  2704. case 'in':return 'ST_Intersects';
  2705. case 'ov':return 'ST_Overlaps';
  2706. case 'to':return 'ST_Touches';
  2707. case 'wi':return 'ST_Within';
  2708. case 'ic':return 'ST_IsClosed';
  2709. case 'is':return 'ST_IsSimple';
  2710. case 'iv':return 'ST_IsValid';
  2711. }
  2712. }
  2713. private function hasSpatialArgument(string $operator): bool
  2714. {
  2715. return in_array($operator, ['ic', 'is', 'iv']) ? false : true;
  2716. }
  2717. private function getSpatialFunctionCall(string $functionName, string $column, bool $hasArgument): string
  2718. {
  2719. switch ($this->driver) {
  2720. case 'mysql':
  2721. case 'pgsql':
  2722. $argument = $hasArgument ? 'ST_GeomFromText(?)' : '';
  2723. return "$functionName($column, $argument)=TRUE";
  2724. case 'sqlsrv':
  2725. $functionName = str_replace('ST_', 'ST', $functionName);
  2726. $argument = $hasArgument ? 'geometry::STGeomFromText(?,0)' : '';
  2727. return "$column.$functionName($argument)=1";
  2728. }
  2729. }
  2730. private function getSpatialConditionSql(ColumnCondition $condition, array &$arguments): string
  2731. {
  2732. $column = $this->quoteColumnName($condition->getColumn());
  2733. $operator = $condition->getOperator();
  2734. $value = $condition->getValue();
  2735. $functionName = $this->getSpatialFunctionName($operator);
  2736. $hasArgument = $this->hasSpatialArgument($operator);
  2737. $sql = $this->getSpatialFunctionCall($functionName, $column, $hasArgument);
  2738. if ($hasArgument) {
  2739. $arguments[] = $value;
  2740. }
  2741. return $sql;
  2742. }
  2743. public function getWhereClause(Condition $condition, array &$arguments): string
  2744. {
  2745. if ($condition instanceof NoCondition) {
  2746. return '';
  2747. }
  2748. return ' WHERE ' . $this->getConditionSql($condition, $arguments);
  2749. }
  2750. }
  2751. // file: src/Tqdev/PhpCrudApi/Database/DataConverter.php
  2752. class DataConverter
  2753. {
  2754. private $driver;
  2755. public function __construct(string $driver)
  2756. {
  2757. $this->driver = $driver;
  2758. }
  2759. private function convertRecordValue($conversion, $value)
  2760. {
  2761. switch ($conversion) {
  2762. case 'boolean':
  2763. return $value ? true : false;
  2764. case 'integer':
  2765. return (int) $value;
  2766. }
  2767. return $value;
  2768. }
  2769. private function getRecordValueConversion(ReflectedColumn $column): string
  2770. {
  2771. if (in_array($this->driver, ['mysql', 'sqlsrv']) && $column->isBoolean()) {
  2772. return 'boolean';
  2773. }
  2774. if ($this->driver == 'sqlsrv' && $column->getType() == 'bigint') {
  2775. return 'integer';
  2776. }
  2777. return 'none';
  2778. }
  2779. public function convertRecords(ReflectedTable $table, array $columnNames, array &$records) /*: void*/
  2780. {
  2781. foreach ($columnNames as $columnName) {
  2782. $column = $table->getColumn($columnName);
  2783. $conversion = $this->getRecordValueConversion($column);
  2784. if ($conversion != 'none') {
  2785. foreach ($records as $i => $record) {
  2786. $value = $records[$i][$columnName];
  2787. if ($value === null) {
  2788. continue;
  2789. }
  2790. $records[$i][$columnName] = $this->convertRecordValue($conversion, $value);
  2791. }
  2792. }
  2793. }
  2794. }
  2795. private function convertInputValue($conversion, $value)
  2796. {
  2797. switch ($conversion) {
  2798. case 'base64url_to_base64':
  2799. return str_pad(strtr($value, '-_', '+/'), ceil(strlen($value) / 4) * 4, '=', STR_PAD_RIGHT);
  2800. }
  2801. return $value;
  2802. }
  2803. private function getInputValueConversion(ReflectedColumn $column): string
  2804. {
  2805. if ($column->isBinary()) {
  2806. return 'base64url_to_base64';
  2807. }
  2808. return 'none';
  2809. }
  2810. public function convertColumnValues(ReflectedTable $table, array &$columnValues) /*: void*/
  2811. {
  2812. $columnNames = array_keys($columnValues);
  2813. foreach ($columnNames as $columnName) {
  2814. $column = $table->getColumn($columnName);
  2815. $conversion = $this->getInputValueConversion($column);
  2816. if ($conversion != 'none') {
  2817. $value = $columnValues[$columnName];
  2818. if ($value !== null) {
  2819. $columnValues[$columnName] = $this->convertInputValue($conversion, $value);
  2820. }
  2821. }
  2822. }
  2823. }
  2824. }
  2825. // file: src/Tqdev/PhpCrudApi/Database/GenericDB.php
  2826. class GenericDB
  2827. {
  2828. private $driver;
  2829. private $database;
  2830. private $pdo;
  2831. private $reflection;
  2832. private $definition;
  2833. private $conditions;
  2834. private $columns;
  2835. private $converter;
  2836. private function getDsn(string $address, int $port, string $database): string
  2837. {
  2838. switch ($this->driver) {
  2839. case 'mysql':return "$this->driver:host=$address;port=$port;dbname=$database;charset=utf8mb4";
  2840. case 'pgsql':return "$this->driver:host=$address port=$port dbname=$database options='--client_encoding=UTF8'";
  2841. case 'sqlsrv':return "$this->driver:Server=$address,$port;Database=$database";
  2842. }
  2843. }
  2844. private function getCommands(): array
  2845. {
  2846. switch ($this->driver) {
  2847. case 'mysql':return [
  2848. 'SET SESSION sql_warnings=1;',
  2849. 'SET NAMES utf8mb4;',
  2850. 'SET SESSION sql_mode = "ANSI,TRADITIONAL";',
  2851. ];
  2852. case 'pgsql':return [
  2853. "SET NAMES 'UTF8';",
  2854. ];
  2855. case 'sqlsrv':return [
  2856. ];
  2857. }
  2858. }
  2859. private function getOptions(): array
  2860. {
  2861. $options = array(
  2862. \PDO::ATTR_ERRMODE => \PDO::ERRMODE_EXCEPTION,
  2863. \PDO::ATTR_DEFAULT_FETCH_MODE => \PDO::FETCH_ASSOC,
  2864. );
  2865. switch ($this->driver) {
  2866. case 'mysql':return $options + [
  2867. \PDO::ATTR_EMULATE_PREPARES => false,
  2868. \PDO::MYSQL_ATTR_FOUND_ROWS => true,
  2869. \PDO::ATTR_PERSISTENT => true,
  2870. ];
  2871. case 'pgsql':return $options + [
  2872. \PDO::ATTR_EMULATE_PREPARES => false,
  2873. \PDO::ATTR_PERSISTENT => true,
  2874. ];
  2875. case 'sqlsrv':return $options + [
  2876. \PDO::SQLSRV_ATTR_DIRECT_QUERY => false,
  2877. \PDO::SQLSRV_ATTR_FETCHES_NUMERIC_TYPE => true,
  2878. ];
  2879. }
  2880. }
  2881. public function __construct(string $driver, string $address, int $port, string $database, string $username, string $password)
  2882. {
  2883. $this->driver = $driver;
  2884. $this->database = $database;
  2885. $dsn = $this->getDsn($address, $port, $database);
  2886. $options = $this->getOptions();
  2887. $this->pdo = new \PDO($dsn, $username, $password, $options);
  2888. $commands = $this->getCommands();
  2889. foreach ($commands as $command) {
  2890. $this->pdo->query($command);
  2891. }
  2892. $this->reflection = new GenericReflection($this->pdo, $driver, $database);
  2893. $this->definition = new GenericDefinition($this->pdo, $driver, $database);
  2894. $this->conditions = new ConditionsBuilder($driver);
  2895. $this->columns = new ColumnsBuilder($driver);
  2896. $this->converter = new DataConverter($driver);
  2897. }
  2898. public function pdo(): \PDO
  2899. {
  2900. return $this->pdo;
  2901. }
  2902. public function reflection(): GenericReflection
  2903. {
  2904. return $this->reflection;
  2905. }
  2906. public function definition(): GenericDefinition
  2907. {
  2908. return $this->definition;
  2909. }
  2910. private function addMiddlewareConditions(string $tableName, Condition $condition): Condition
  2911. {
  2912. $condition1 = VariableStore::get("authorization.conditions.$tableName");
  2913. if ($condition1) {
  2914. $condition = $condition->_and($condition1);
  2915. }
  2916. $condition2 = VariableStore::get("multiTenancy.conditions.$tableName");
  2917. if ($condition2) {
  2918. $condition = $condition->_and($condition2);
  2919. }
  2920. return $condition;
  2921. }
  2922. public function createSingle(ReflectedTable $table, array $columnValues) /*: ?String*/
  2923. {
  2924. $this->converter->convertColumnValues($table, $columnValues);
  2925. $insertColumns = $this->columns->getInsert($table, $columnValues);
  2926. $tableName = $table->getName();
  2927. $pkName = $table->getPk()->getName();
  2928. $parameters = array_values($columnValues);
  2929. $sql = 'INSERT INTO "' . $tableName . '" ' . $insertColumns;
  2930. $stmt = $this->query($sql, $parameters);
  2931. if (isset($columnValues[$pkName])) {
  2932. return $columnValues[$pkName];
  2933. }
  2934. switch ($this->driver) {
  2935. case 'mysql':
  2936. $stmt = $this->query('SELECT LAST_INSERT_ID()', []);
  2937. break;
  2938. }
  2939. $pkValue = $stmt->fetchColumn(0);
  2940. if ($this->driver == 'sqlsrv' && $table->getPk()->getType() == 'bigint') {
  2941. return (int) $pkValue;
  2942. }
  2943. return $pkValue;
  2944. }
  2945. public function selectSingle(ReflectedTable $table, array $columnNames, string $id) /*: ?array*/
  2946. {
  2947. $selectColumns = $this->columns->getSelect($table, $columnNames);
  2948. $tableName = $table->getName();
  2949. $condition = new ColumnCondition($table->getPk(), 'eq', $id);
  2950. $condition = $this->addMiddlewareConditions($tableName, $condition);
  2951. $parameters = array();
  2952. $whereClause = $this->conditions->getWhereClause($condition, $parameters);
  2953. $sql = 'SELECT ' . $selectColumns . ' FROM "' . $tableName . '" ' . $whereClause;
  2954. $stmt = $this->query($sql, $parameters);
  2955. $record = $stmt->fetch() ?: null;
  2956. if ($record === null) {
  2957. return null;
  2958. }
  2959. $records = array($record);
  2960. $this->converter->convertRecords($table, $columnNames, $records);
  2961. return $records[0];
  2962. }
  2963. public function selectMultiple(ReflectedTable $table, array $columnNames, array $ids): array
  2964. {
  2965. if (count($ids) == 0) {
  2966. return [];
  2967. }
  2968. $selectColumns = $this->columns->getSelect($table, $columnNames);
  2969. $tableName = $table->getName();
  2970. $condition = new ColumnCondition($table->getPk(), 'in', implode(',', $ids));
  2971. $condition = $this->addMiddlewareConditions($tableName, $condition);
  2972. $parameters = array();
  2973. $whereClause = $this->conditions->getWhereClause($condition, $parameters);
  2974. $sql = 'SELECT ' . $selectColumns . ' FROM "' . $tableName . '" ' . $whereClause;
  2975. $stmt = $this->query($sql, $parameters);
  2976. $records = $stmt->fetchAll();
  2977. $this->converter->convertRecords($table, $columnNames, $records);
  2978. return $records;
  2979. }
  2980. public function selectCount(ReflectedTable $table, Condition $condition): int
  2981. {
  2982. $tableName = $table->getName();
  2983. $condition = $this->addMiddlewareConditions($tableName, $condition);
  2984. $parameters = array();
  2985. $whereClause = $this->conditions->getWhereClause($condition, $parameters);
  2986. $sql = 'SELECT COUNT(*) FROM "' . $tableName . '"' . $whereClause;
  2987. $stmt = $this->query($sql, $parameters);
  2988. return $stmt->fetchColumn(0);
  2989. }
  2990. public function selectAll(ReflectedTable $table, array $columnNames, Condition $condition, array $columnOrdering, int $offset, int $limit): array
  2991. {
  2992. if ($limit == 0) {
  2993. return array();
  2994. }
  2995. $selectColumns = $this->columns->getSelect($table, $columnNames);
  2996. $tableName = $table->getName();
  2997. $condition = $this->addMiddlewareConditions($tableName, $condition);
  2998. $parameters = array();
  2999. $whereClause = $this->conditions->getWhereClause($condition, $parameters);
  3000. $orderBy = $this->columns->getOrderBy($table, $columnOrdering);
  3001. $offsetLimit = $this->columns->getOffsetLimit($offset, $limit);
  3002. $sql = 'SELECT ' . $selectColumns . ' FROM "' . $tableName . '"' . $whereClause . $orderBy . $offsetLimit;
  3003. $stmt = $this->query($sql, $parameters);
  3004. $records = $stmt->fetchAll();
  3005. $this->converter->convertRecords($table, $columnNames, $records);
  3006. return $records;
  3007. }
  3008. public function updateSingle(ReflectedTable $table, array $columnValues, string $id)
  3009. {
  3010. if (count($columnValues) == 0) {
  3011. return 0;
  3012. }
  3013. $this->converter->convertColumnValues($table, $columnValues);
  3014. $updateColumns = $this->columns->getUpdate($table, $columnValues);
  3015. $tableName = $table->getName();
  3016. $condition = new ColumnCondition($table->getPk(), 'eq', $id);
  3017. $condition = $this->addMiddlewareConditions($tableName, $condition);
  3018. $parameters = array_values($columnValues);
  3019. $whereClause = $this->conditions->getWhereClause($condition, $parameters);
  3020. $sql = 'UPDATE "' . $tableName . '" SET ' . $updateColumns . $whereClause;
  3021. $stmt = $this->query($sql, $parameters);
  3022. return $stmt->rowCount();
  3023. }
  3024. public function deleteSingle(ReflectedTable $table, string $id)
  3025. {
  3026. $tableName = $table->getName();
  3027. $condition = new ColumnCondition($table->getPk(), 'eq', $id);
  3028. $condition = $this->addMiddlewareConditions($tableName, $condition);
  3029. $parameters = array();
  3030. $whereClause = $this->conditions->getWhereClause($condition, $parameters);
  3031. $sql = 'DELETE FROM "' . $tableName . '" ' . $whereClause;
  3032. $stmt = $this->query($sql, $parameters);
  3033. return $stmt->rowCount();
  3034. }
  3035. public function incrementSingle(ReflectedTable $table, array $columnValues, string $id)
  3036. {
  3037. if (count($columnValues) == 0) {
  3038. return 0;
  3039. }
  3040. $this->converter->convertColumnValues($table, $columnValues);
  3041. $updateColumns = $this->columns->getIncrement($table, $columnValues);
  3042. $tableName = $table->getName();
  3043. $condition = new ColumnCondition($table->getPk(), 'eq', $id);
  3044. $condition = $this->addMiddlewareConditions($tableName, $condition);
  3045. $parameters = array_values($columnValues);
  3046. $whereClause = $this->conditions->getWhereClause($condition, $parameters);
  3047. $sql = 'UPDATE "' . $tableName . '" SET ' . $updateColumns . $whereClause;
  3048. $stmt = $this->query($sql, $parameters);
  3049. return $stmt->rowCount();
  3050. }
  3051. private function query(string $sql, array $parameters): \PDOStatement
  3052. {
  3053. $stmt = $this->pdo->prepare($sql);
  3054. $stmt->execute($parameters);
  3055. return $stmt;
  3056. }
  3057. }
  3058. // file: src/Tqdev/PhpCrudApi/Database/GenericDefinition.php
  3059. class GenericDefinition
  3060. {
  3061. private $pdo;
  3062. private $driver;
  3063. private $database;
  3064. private $typeConverter;
  3065. private $reflection;
  3066. public function __construct(\PDO $pdo, string $driver, string $database)
  3067. {
  3068. $this->pdo = $pdo;
  3069. $this->driver = $driver;
  3070. $this->database = $database;
  3071. $this->typeConverter = new TypeConverter($driver);
  3072. $this->reflection = new GenericReflection($pdo, $driver, $database);
  3073. }
  3074. private function quote(string $identifier): string
  3075. {
  3076. return '"' . str_replace('"', '', $identifier) . '"';
  3077. }
  3078. public function getColumnType(ReflectedColumn $column, bool $update): string
  3079. {
  3080. if ($this->driver == 'pgsql' && !$update && $column->getPk() && $this->canAutoIncrement($column)) {
  3081. return 'serial';
  3082. }
  3083. $type = $this->typeConverter->fromJdbc($column->getType());
  3084. if ($column->hasPrecision() && $column->hasScale()) {
  3085. $size = '(' . $column->getPrecision() . ',' . $column->getScale() . ')';
  3086. } else if ($column->hasPrecision()) {
  3087. $size = '(' . $column->getPrecision() . ')';
  3088. } else if ($column->hasLength()) {
  3089. $size = '(' . $column->getLength() . ')';
  3090. } else {
  3091. $size = '';
  3092. }
  3093. $null = $this->getColumnNullType($column, $update);
  3094. $auto = $this->getColumnAutoIncrement($column, $update);
  3095. return $type . $size . $null . $auto;
  3096. }
  3097. private function getPrimaryKey(string $tableName): string
  3098. {
  3099. $pks = $this->reflection->getTablePrimaryKeys($tableName);
  3100. if (count($pks) == 1) {
  3101. return $pks[0];
  3102. }
  3103. return "";
  3104. }
  3105. private function canAutoIncrement(ReflectedColumn $column): bool
  3106. {
  3107. return in_array($column->getType(), ['integer', 'bigint']);
  3108. }
  3109. private function getColumnAutoIncrement(ReflectedColumn $column, bool $update): string
  3110. {
  3111. if (!$this->canAutoIncrement($column)) {
  3112. return '';
  3113. }
  3114. switch ($this->driver) {
  3115. case 'mysql':
  3116. return $column->getPk() ? ' AUTO_INCREMENT' : '';
  3117. case 'pgsql':
  3118. case 'sqlsrv':
  3119. return '';
  3120. }
  3121. }
  3122. private function getColumnNullType(ReflectedColumn $column, bool $update): string
  3123. {
  3124. if ($this->driver == 'pgsql' && $update) {
  3125. return '';
  3126. }
  3127. return $column->getNullable() ? ' NULL' : ' NOT NULL';
  3128. }
  3129. private function getTableRenameSQL(string $tableName, string $newTableName): string
  3130. {
  3131. $p1 = $this->quote($tableName);
  3132. $p2 = $this->quote($newTableName);
  3133. switch ($this->driver) {
  3134. case 'mysql':
  3135. return "RENAME TABLE $p1 TO $p2";
  3136. case 'pgsql':
  3137. return "ALTER TABLE $p1 RENAME TO $p2";
  3138. case 'sqlsrv':
  3139. return "EXEC sp_rename $p1, $p2";
  3140. }
  3141. }
  3142. private function getColumnRenameSQL(string $tableName, string $columnName, ReflectedColumn $newColumn): string
  3143. {
  3144. $p1 = $this->quote($tableName);
  3145. $p2 = $this->quote($columnName);
  3146. $p3 = $this->quote($newColumn->getName());
  3147. switch ($this->driver) {
  3148. case 'mysql':
  3149. $p4 = $this->getColumnType($newColumn, true);
  3150. return "ALTER TABLE $p1 CHANGE $p2 $p3 $p4";
  3151. case 'pgsql':
  3152. return "ALTER TABLE $p1 RENAME COLUMN $p2 TO $p3";
  3153. case 'sqlsrv':
  3154. $p4 = $this->quote($tableName . '.' . $columnName);
  3155. return "EXEC sp_rename $p4, $p3, 'COLUMN'";
  3156. }
  3157. }
  3158. private function getColumnRetypeSQL(string $tableName, string $columnName, ReflectedColumn $newColumn): string
  3159. {
  3160. $p1 = $this->quote($tableName);
  3161. $p2 = $this->quote($columnName);
  3162. $p3 = $this->quote($newColumn->getName());
  3163. $p4 = $this->getColumnType($newColumn, true);
  3164. switch ($this->driver) {
  3165. case 'mysql':
  3166. return "ALTER TABLE $p1 CHANGE $p2 $p3 $p4";
  3167. case 'pgsql':
  3168. return "ALTER TABLE $p1 ALTER COLUMN $p3 TYPE $p4";
  3169. case 'sqlsrv':
  3170. return "ALTER TABLE $p1 ALTER COLUMN $p3 $p4";
  3171. }
  3172. }
  3173. private function getSetColumnNullableSQL(string $tableName, string $columnName, ReflectedColumn $newColumn): string
  3174. {
  3175. $p1 = $this->quote($tableName);
  3176. $p2 = $this->quote($columnName);
  3177. $p3 = $this->quote($newColumn->getName());
  3178. $p4 = $this->getColumnType($newColumn, true);
  3179. switch ($this->driver) {
  3180. case 'mysql':
  3181. return "ALTER TABLE $p1 CHANGE $p2 $p3 $p4";
  3182. case 'pgsql':
  3183. $p5 = $newColumn->getNullable() ? 'DROP NOT NULL' : 'SET NOT NULL';
  3184. return "ALTER TABLE $p1 ALTER COLUMN $p2 $p5";
  3185. case 'sqlsrv':
  3186. return "ALTER TABLE $p1 ALTER COLUMN $p2 $p4";
  3187. }
  3188. }
  3189. private function getSetColumnPkConstraintSQL(string $tableName, string $columnName, ReflectedColumn $newColumn): string
  3190. {
  3191. $p1 = $this->quote($tableName);
  3192. $p2 = $this->quote($columnName);
  3193. $p3 = $this->quote($tableName . '_pkey');
  3194. switch ($this->driver) {
  3195. case 'mysql':
  3196. $p4 = $newColumn->getPk() ? "ADD PRIMARY KEY ($p2)" : 'DROP PRIMARY KEY';
  3197. return "ALTER TABLE $p1 $p4";
  3198. case 'pgsql':
  3199. case 'sqlsrv':
  3200. $p4 = $newColumn->getPk() ? "ADD CONSTRAINT $p3 PRIMARY KEY ($p2)" : "DROP CONSTRAINT $p3";
  3201. return "ALTER TABLE $p1 $p4";
  3202. }
  3203. }
  3204. private function getSetColumnPkSequenceSQL(string $tableName, string $columnName, ReflectedColumn $newColumn): string
  3205. {
  3206. $p1 = $this->quote($tableName);
  3207. $p2 = $this->quote($columnName);
  3208. $p3 = $this->quote($tableName . '_' . $columnName . '_seq');
  3209. switch ($this->driver) {
  3210. case 'mysql':
  3211. return "select 1";
  3212. case 'pgsql':
  3213. return $newColumn->getPk() ? "CREATE SEQUENCE $p3 OWNED BY $p1.$p2" : "DROP SEQUENCE $p3";
  3214. case 'sqlsrv':
  3215. return $newColumn->getPk() ? "CREATE SEQUENCE $p3" : "DROP SEQUENCE $p3";
  3216. }
  3217. }
  3218. private function getSetColumnPkSequenceStartSQL(string $tableName, string $columnName, ReflectedColumn $newColumn): string
  3219. {
  3220. $p1 = $this->quote($tableName);
  3221. $p2 = $this->quote($columnName);
  3222. switch ($this->driver) {
  3223. case 'mysql':
  3224. return "select 1";
  3225. case 'pgsql':
  3226. $p3 = $this->pdo->quote($tableName . '_' . $columnName . '_seq');
  3227. return "SELECT setval($p3, (SELECT max($p2)+1 FROM $p1));";
  3228. case 'sqlsrv':
  3229. $p3 = $this->quote($tableName . '_' . $columnName . '_seq');
  3230. $p4 = $this->pdo->query("SELECT max($p2)+1 FROM $p1")->fetchColumn();
  3231. return "ALTER SEQUENCE $p3 RESTART WITH $p4";
  3232. }
  3233. }
  3234. private function getSetColumnPkDefaultSQL(string $tableName, string $columnName, ReflectedColumn $newColumn): string
  3235. {
  3236. $p1 = $this->quote($tableName);
  3237. $p2 = $this->quote($columnName);
  3238. switch ($this->driver) {
  3239. case 'mysql':
  3240. $p3 = $this->quote($newColumn->getName());
  3241. $p4 = $this->getColumnType($newColumn, true);
  3242. return "ALTER TABLE $p1 CHANGE $p2 $p3 $p4";
  3243. case 'pgsql':
  3244. if ($newColumn->getPk()) {
  3245. $p3 = $this->pdo->quote($tableName . '_' . $columnName . '_seq');
  3246. $p4 = "SET DEFAULT nextval($p3)";
  3247. } else {
  3248. $p4 = 'DROP DEFAULT';
  3249. }
  3250. return "ALTER TABLE $p1 ALTER COLUMN $p2 $p4";
  3251. case 'sqlsrv':
  3252. $p3 = $this->quote($tableName . '_' . $columnName . '_seq');
  3253. $p4 = $this->quote($tableName . '_' . $columnName . '_def');
  3254. if ($newColumn->getPk()) {
  3255. return "ALTER TABLE $p1 ADD CONSTRAINT $p4 DEFAULT NEXT VALUE FOR $p3 FOR $p2";
  3256. } else {
  3257. return "ALTER TABLE $p1 DROP CONSTRAINT $p4";
  3258. }
  3259. }
  3260. }
  3261. private function getAddColumnFkConstraintSQL(string $tableName, string $columnName, ReflectedColumn $newColumn): string
  3262. {
  3263. $p1 = $this->quote($tableName);
  3264. $p2 = $this->quote($columnName);
  3265. $p3 = $this->quote($tableName . '_' . $columnName . '_fkey');
  3266. $p4 = $this->quote($newColumn->getFk());
  3267. $p5 = $this->quote($this->getPrimaryKey($newColumn->getFk()));
  3268. return "ALTER TABLE $p1 ADD CONSTRAINT $p3 FOREIGN KEY ($p2) REFERENCES $p4 ($p5)";
  3269. }
  3270. private function getRemoveColumnFkConstraintSQL(string $tableName, string $columnName, ReflectedColumn $newColumn): string
  3271. {
  3272. $p1 = $this->quote($tableName);
  3273. $p2 = $this->quote($tableName . '_' . $columnName . '_fkey');
  3274. switch ($this->driver) {
  3275. case 'mysql':
  3276. return "ALTER TABLE $p1 DROP FOREIGN KEY $p2";
  3277. case 'pgsql':
  3278. case 'sqlsrv':
  3279. return "ALTER TABLE $p1 DROP CONSTRAINT $p2";
  3280. }
  3281. }
  3282. private function getAddTableSQL(ReflectedTable $newTable): string
  3283. {
  3284. $tableName = $newTable->getName();
  3285. $p1 = $this->quote($tableName);
  3286. $fields = [];
  3287. $constraints = [];
  3288. foreach ($newTable->getColumnNames() as $columnName) {
  3289. $pkColumn = $this->getPrimaryKey($tableName);
  3290. $newColumn = $newTable->getColumn($columnName);
  3291. $f1 = $this->quote($columnName);
  3292. $f2 = $this->getColumnType($newColumn, false);
  3293. $f3 = $this->quote($tableName . '_' . $columnName . '_fkey');
  3294. $f4 = $this->quote($newColumn->getFk());
  3295. $f5 = $this->quote($this->getPrimaryKey($newColumn->getFk()));
  3296. $f6 = $this->quote($tableName . '_' . $pkColumn . '_pkey');
  3297. $fields[] = "$f1 $f2";
  3298. if ($newColumn->getPk()) {
  3299. $constraints[] = "CONSTRAINT $f6 PRIMARY KEY ($f1)";
  3300. }
  3301. if ($newColumn->getFk()) {
  3302. $constraints[] = "CONSTRAINT $f3 FOREIGN KEY ($f1) REFERENCES $f4 ($f5)";
  3303. }
  3304. }
  3305. $p2 = implode(',', array_merge($fields, $constraints));
  3306. return "CREATE TABLE $p1 ($p2);";
  3307. }
  3308. private function getAddColumnSQL(string $tableName, ReflectedColumn $newColumn): string
  3309. {
  3310. $p1 = $this->quote($tableName);
  3311. $p2 = $this->quote($newColumn->getName());
  3312. $p3 = $this->getColumnType($newColumn, false);
  3313. switch ($this->driver) {
  3314. case 'mysql':
  3315. case 'pgsql':
  3316. return "ALTER TABLE $p1 ADD COLUMN $p2 $p3";
  3317. case 'sqlsrv':
  3318. return "ALTER TABLE $p1 ADD $p2 $p3";
  3319. }
  3320. }
  3321. private function getRemoveTableSQL(string $tableName): string
  3322. {
  3323. $p1 = $this->quote($tableName);
  3324. switch ($this->driver) {
  3325. case 'mysql':
  3326. case 'pgsql':
  3327. return "DROP TABLE $p1 CASCADE;";
  3328. case 'sqlsrv':
  3329. return "DROP TABLE $p1;";
  3330. }
  3331. }
  3332. private function getRemoveColumnSQL(string $tableName, string $columnName): string
  3333. {
  3334. $p1 = $this->quote($tableName);
  3335. $p2 = $this->quote($columnName);
  3336. switch ($this->driver) {
  3337. case 'mysql':
  3338. case 'pgsql':
  3339. return "ALTER TABLE $p1 DROP COLUMN $p2 CASCADE;";
  3340. case 'sqlsrv':
  3341. return "ALTER TABLE $p1 DROP COLUMN $p2;";
  3342. }
  3343. }
  3344. public function renameTable(string $tableName, string $newTableName)
  3345. {
  3346. $sql = $this->getTableRenameSQL($tableName, $newTableName);
  3347. return $this->query($sql);
  3348. }
  3349. public function renameColumn(string $tableName, string $columnName, ReflectedColumn $newColumn)
  3350. {
  3351. $sql = $this->getColumnRenameSQL($tableName, $columnName, $newColumn);
  3352. return $this->query($sql);
  3353. }
  3354. public function retypeColumn(string $tableName, string $columnName, ReflectedColumn $newColumn)
  3355. {
  3356. $sql = $this->getColumnRetypeSQL($tableName, $columnName, $newColumn);
  3357. return $this->query($sql);
  3358. }
  3359. public function setColumnNullable(string $tableName, string $columnName, ReflectedColumn $newColumn)
  3360. {
  3361. $sql = $this->getSetColumnNullableSQL($tableName, $columnName, $newColumn);
  3362. return $this->query($sql);
  3363. }
  3364. public function addColumnPrimaryKey(string $tableName, string $columnName, ReflectedColumn $newColumn)
  3365. {
  3366. $sql = $this->getSetColumnPkConstraintSQL($tableName, $columnName, $newColumn);
  3367. $this->query($sql);
  3368. if ($this->canAutoIncrement($newColumn)) {
  3369. $sql = $this->getSetColumnPkSequenceSQL($tableName, $columnName, $newColumn);
  3370. $this->query($sql);
  3371. $sql = $this->getSetColumnPkSequenceStartSQL($tableName, $columnName, $newColumn);
  3372. $this->query($sql);
  3373. $sql = $this->getSetColumnPkDefaultSQL($tableName, $columnName, $newColumn);
  3374. $this->query($sql);
  3375. }
  3376. return true;
  3377. }
  3378. public function removeColumnPrimaryKey(string $tableName, string $columnName, ReflectedColumn $newColumn)
  3379. {
  3380. if ($this->canAutoIncrement($newColumn)) {
  3381. $sql = $this->getSetColumnPkDefaultSQL($tableName, $columnName, $newColumn);
  3382. $this->query($sql);
  3383. $sql = $this->getSetColumnPkSequenceSQL($tableName, $columnName, $newColumn);
  3384. $this->query($sql);
  3385. }
  3386. $sql = $this->getSetColumnPkConstraintSQL($tableName, $columnName, $newColumn);
  3387. $this->query($sql);
  3388. return true;
  3389. }
  3390. public function addColumnForeignKey(string $tableName, string $columnName, ReflectedColumn $newColumn)
  3391. {
  3392. $sql = $this->getAddColumnFkConstraintSQL($tableName, $columnName, $newColumn);
  3393. return $this->query($sql);
  3394. }
  3395. public function removeColumnForeignKey(string $tableName, string $columnName, ReflectedColumn $newColumn)
  3396. {
  3397. $sql = $this->getRemoveColumnFkConstraintSQL($tableName, $columnName, $newColumn);
  3398. return $this->query($sql);
  3399. }
  3400. public function addTable(ReflectedTable $newTable)
  3401. {
  3402. $sql = $this->getAddTableSQL($newTable);
  3403. return $this->query($sql);
  3404. }
  3405. public function addColumn(string $tableName, ReflectedColumn $newColumn)
  3406. {
  3407. $sql = $this->getAddColumnSQL($tableName, $newColumn);
  3408. return $this->query($sql);
  3409. }
  3410. public function removeTable(string $tableName)
  3411. {
  3412. $sql = $this->getRemoveTableSQL($tableName);
  3413. return $this->query($sql);
  3414. }
  3415. public function removeColumn(string $tableName, string $columnName)
  3416. {
  3417. $sql = $this->getRemoveColumnSQL($tableName, $columnName);
  3418. return $this->query($sql);
  3419. }
  3420. private function query(string $sql): bool
  3421. {
  3422. $stmt = $this->pdo->prepare($sql);
  3423. return $stmt->execute();
  3424. }
  3425. }
  3426. // file: src/Tqdev/PhpCrudApi/Database/GenericReflection.php
  3427. class GenericReflection
  3428. {
  3429. private $pdo;
  3430. private $driver;
  3431. private $database;
  3432. private $typeConverter;
  3433. public function __construct(\PDO $pdo, string $driver, string $database)
  3434. {
  3435. $this->pdo = $pdo;
  3436. $this->driver = $driver;
  3437. $this->database = $database;
  3438. $this->typeConverter = new TypeConverter($driver);
  3439. }
  3440. public function getIgnoredTables(): array
  3441. {
  3442. switch ($this->driver) {
  3443. case 'mysql':return [];
  3444. case 'pgsql':return ['spatial_ref_sys', 'raster_columns', 'raster_overviews', 'geography_columns', 'geometry_columns'];
  3445. case 'sqlsrv':return [];
  3446. }
  3447. }
  3448. private function getTablesSQL(): string
  3449. {
  3450. switch ($this->driver) {
  3451. case 'mysql':return 'SELECT "TABLE_NAME", "TABLE_TYPE" FROM "INFORMATION_SCHEMA"."TABLES" WHERE "TABLE_TYPE" IN (\'BASE TABLE\' , \'VIEW\') AND "TABLE_SCHEMA" = ? ORDER BY BINARY "TABLE_NAME"';
  3452. case 'pgsql':return 'SELECT c.relname as "TABLE_NAME", c.relkind as "TABLE_TYPE" FROM pg_catalog.pg_class c LEFT JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace WHERE c.relkind IN (\'r\', \'v\') AND n.nspname <> \'pg_catalog\' AND n.nspname <> \'information_schema\' AND n.nspname !~ \'^pg_toast\' AND pg_catalog.pg_table_is_visible(c.oid) AND \'\' <> ? ORDER BY "TABLE_NAME";';
  3453. case 'sqlsrv':return 'SELECT o.name as "TABLE_NAME", o.xtype as "TABLE_TYPE" FROM sysobjects o WHERE o.xtype IN (\'U\', \'V\') ORDER BY "TABLE_NAME"';
  3454. }
  3455. }
  3456. private function getTableColumnsSQL(): string
  3457. {
  3458. switch ($this->driver) {
  3459. case 'mysql':return 'SELECT "COLUMN_NAME", "IS_NULLABLE", "DATA_TYPE", "CHARACTER_MAXIMUM_LENGTH", "NUMERIC_PRECISION", "NUMERIC_SCALE" FROM "INFORMATION_SCHEMA"."COLUMNS" WHERE "TABLE_NAME" = ? AND "TABLE_SCHEMA" = ?';
  3460. case 'pgsql':return 'SELECT a.attname AS "COLUMN_NAME", case when a.attnotnull then \'NO\' else \'YES\' end as "IS_NULLABLE", pg_catalog.format_type(a.atttypid, -1) as "DATA_TYPE", case when a.atttypmod < 0 then NULL else a.atttypmod-4 end as "CHARACTER_MAXIMUM_LENGTH", case when a.atttypid != 1700 then NULL else ((a.atttypmod - 4) >> 16) & 65535 end as "NUMERIC_PRECISION", case when a.atttypid != 1700 then NULL else (a.atttypmod - 4) & 65535 end as "NUMERIC_SCALE" FROM pg_attribute a JOIN pg_class pgc ON pgc.oid = a.attrelid WHERE pgc.relname = ? AND \'\' <> ? AND a.attnum > 0 AND NOT a.attisdropped;';
  3461. case 'sqlsrv':return 'SELECT c.name AS "COLUMN_NAME", c.is_nullable AS "IS_NULLABLE", t.Name AS "DATA_TYPE", (c.max_length/2) AS "CHARACTER_MAXIMUM_LENGTH", c.precision AS "NUMERIC_PRECISION", c.scale AS "NUMERIC_SCALE" FROM sys.columns c INNER JOIN sys.types t ON c.user_type_id = t.user_type_id WHERE c.object_id = OBJECT_ID(?) AND \'\' <> ?';
  3462. }
  3463. }
  3464. private function getTablePrimaryKeysSQL(): string
  3465. {
  3466. switch ($this->driver) {
  3467. case 'mysql':return 'SELECT "COLUMN_NAME" FROM "INFORMATION_SCHEMA"."KEY_COLUMN_USAGE" WHERE "CONSTRAINT_NAME" = \'PRIMARY\' AND "TABLE_NAME" = ? AND "TABLE_SCHEMA" = ?';
  3468. case 'pgsql':return 'SELECT a.attname AS "COLUMN_NAME" FROM pg_attribute a JOIN pg_constraint c ON (c.conrelid, c.conkey[1]) = (a.attrelid, a.attnum) JOIN pg_class pgc ON pgc.oid = a.attrelid WHERE pgc.relname = ? AND \'\' <> ? AND c.contype = \'p\'';
  3469. case 'sqlsrv':return 'SELECT c.NAME as "COLUMN_NAME" FROM sys.key_constraints kc inner join sys.objects t on t.object_id = kc.parent_object_id INNER JOIN sys.index_columns ic ON kc.parent_object_id = ic.object_id and kc.unique_index_id = ic.index_id INNER JOIN sys.columns c ON ic.object_id = c.object_id AND ic.column_id = c.column_id WHERE kc.type = \'PK\' and t.object_id = OBJECT_ID(?) and \'\' <> ?';
  3470. }
  3471. }
  3472. private function getTableForeignKeysSQL(): string
  3473. {
  3474. switch ($this->driver) {
  3475. case 'mysql':return 'SELECT "COLUMN_NAME", "REFERENCED_TABLE_NAME" FROM "INFORMATION_SCHEMA"."KEY_COLUMN_USAGE" WHERE "REFERENCED_TABLE_NAME" IS NOT NULL AND "TABLE_NAME" = ? AND "TABLE_SCHEMA" = ?';
  3476. case 'pgsql':return 'SELECT a.attname AS "COLUMN_NAME", c.confrelid::regclass::text AS "REFERENCED_TABLE_NAME" FROM pg_attribute a JOIN pg_constraint c ON (c.conrelid, c.conkey[1]) = (a.attrelid, a.attnum) JOIN pg_class pgc ON pgc.oid = a.attrelid WHERE pgc.relname = ? AND \'\' <> ? AND c.contype = \'f\'';
  3477. case 'sqlsrv':return 'SELECT COL_NAME(fc.parent_object_id, fc.parent_column_id) AS "COLUMN_NAME", OBJECT_NAME (f.referenced_object_id) AS "REFERENCED_TABLE_NAME" FROM sys.foreign_keys AS f INNER JOIN sys.foreign_key_columns AS fc ON f.OBJECT_ID = fc.constraint_object_id WHERE f.parent_object_id = OBJECT_ID(?) and \'\' <> ?';
  3478. }
  3479. }
  3480. public function getDatabaseName(): string
  3481. {
  3482. return $this->database;
  3483. }
  3484. public function getTables(): array
  3485. {
  3486. $sql = $this->getTablesSQL();
  3487. $results = $this->query($sql, [$this->database]);
  3488. foreach ($results as &$result) {
  3489. switch ($this->driver) {
  3490. case 'mysql':
  3491. $map = ['BASE TABLE' => 'table', 'VIEW' => 'view'];
  3492. $result['TABLE_TYPE'] = $map[$result['TABLE_TYPE']];
  3493. break;
  3494. case 'pgsql':
  3495. $map = ['r' => 'table', 'v' => 'view'];
  3496. $result['TABLE_TYPE'] = $map[$result['TABLE_TYPE']];
  3497. break;
  3498. case 'sqlsrv':
  3499. $map = ['U' => 'table', 'V' => 'view'];
  3500. $result['TABLE_TYPE'] = $map[trim($result['TABLE_TYPE'])];
  3501. break;
  3502. }
  3503. }
  3504. return $results;
  3505. }
  3506. public function getTableColumns(string $tableName, string $type): array
  3507. {
  3508. $sql = $this->getTableColumnsSQL();
  3509. $results = $this->query($sql, [$tableName, $this->database]);
  3510. if ($type == 'view') {
  3511. foreach ($results as &$result) {
  3512. $result['IS_NULLABLE'] = false;
  3513. }
  3514. }
  3515. return $results;
  3516. }
  3517. public function getTablePrimaryKeys(string $tableName): array
  3518. {
  3519. $sql = $this->getTablePrimaryKeysSQL();
  3520. $results = $this->query($sql, [$tableName, $this->database]);
  3521. $primaryKeys = [];
  3522. foreach ($results as $result) {
  3523. $primaryKeys[] = $result['COLUMN_NAME'];
  3524. }
  3525. return $primaryKeys;
  3526. }
  3527. public function getTableForeignKeys(string $tableName): array
  3528. {
  3529. $sql = $this->getTableForeignKeysSQL();
  3530. $results = $this->query($sql, [$tableName, $this->database]);
  3531. $foreignKeys = [];
  3532. foreach ($results as $result) {
  3533. $foreignKeys[$result['COLUMN_NAME']] = $result['REFERENCED_TABLE_NAME'];
  3534. }
  3535. return $foreignKeys;
  3536. }
  3537. public function toJdbcType(string $type, int $size): string
  3538. {
  3539. return $this->typeConverter->toJdbc($type, $size);
  3540. }
  3541. private function query(string $sql, array $parameters): array
  3542. {
  3543. $stmt = $this->pdo->prepare($sql);
  3544. $stmt->execute($parameters);
  3545. return $stmt->fetchAll();
  3546. }
  3547. }
  3548. // file: src/Tqdev/PhpCrudApi/Database/TypeConverter.php
  3549. class TypeConverter
  3550. {
  3551. private $driver;
  3552. public function __construct(string $driver)
  3553. {
  3554. $this->driver = $driver;
  3555. }
  3556. private $fromJdbc = [
  3557. 'mysql' => [
  3558. 'clob' => 'longtext',
  3559. 'boolean' => 'bit',
  3560. 'blob' => 'longblob',
  3561. 'timestamp' => 'datetime',
  3562. ],
  3563. 'pgsql' => [
  3564. 'clob' => 'text',
  3565. 'blob' => 'bytea',
  3566. ],
  3567. 'sqlsrv' => [
  3568. 'boolean' => 'bit',
  3569. 'varchar' => 'nvarchar',
  3570. 'clob' => 'ntext',
  3571. 'blob' => 'image',
  3572. ],
  3573. ];
  3574. private $toJdbc = [
  3575. 'simplified' => [
  3576. 'char' => 'varchar',
  3577. 'longvarchar' => 'clob',
  3578. 'nchar' => 'varchar',
  3579. 'nvarchar' => 'varchar',
  3580. 'longnvarchar' => 'clob',
  3581. 'binary' => 'varbinary',
  3582. 'longvarbinary' => 'blob',
  3583. 'tinyint' => 'integer',
  3584. 'smallint' => 'integer',
  3585. 'real' => 'float',
  3586. 'numeric' => 'decimal',
  3587. 'time_with_timezone' => 'time',
  3588. 'timestamp_with_timezone' => 'timestamp',
  3589. ],
  3590. 'mysql' => [
  3591. 'bit' => 'boolean',
  3592. 'tinyblob' => 'blob',
  3593. 'mediumblob' => 'blob',
  3594. 'longblob' => 'blob',
  3595. 'tinytext' => 'clob',
  3596. 'mediumtext' => 'clob',
  3597. 'longtext' => 'clob',
  3598. 'text' => 'clob',
  3599. 'mediumint' => 'integer',
  3600. 'int' => 'integer',
  3601. 'polygon' => 'geometry',
  3602. 'point' => 'geometry',
  3603. 'datetime' => 'timestamp',
  3604. 'year' => 'integer',
  3605. 'enum' => 'varchar',
  3606. 'json' => 'clob',
  3607. ],
  3608. 'pgsql' => [
  3609. 'bigserial' => 'bigint',
  3610. 'bit varying' => 'bit',
  3611. 'box' => 'geometry',
  3612. 'bytea' => 'blob',
  3613. 'character varying' => 'varchar',
  3614. 'character' => 'char',
  3615. 'cidr' => 'varchar',
  3616. 'circle' => 'geometry',
  3617. 'double precision' => 'double',
  3618. 'inet' => 'integer',
  3619. 'json' => 'clob',
  3620. 'jsonb' => 'clob',
  3621. 'line' => 'geometry',
  3622. 'lseg' => 'geometry',
  3623. 'macaddr' => 'varchar',
  3624. 'money' => 'decimal',
  3625. 'path' => 'geometry',
  3626. 'point' => 'geometry',
  3627. 'polygon' => 'geometry',
  3628. 'real' => 'float',
  3629. 'serial' => 'integer',
  3630. 'text' => 'clob',
  3631. 'time without time zone' => 'time',
  3632. 'time with time zone' => 'time_with_timezone',
  3633. 'timestamp without time zone' => 'timestamp',
  3634. 'timestamp with time zone' => 'timestamp_with_timezone',
  3635. 'uuid' => 'char',
  3636. 'xml' => 'clob',
  3637. ],
  3638. 'sqlsrv' => [
  3639. 'varbinary(0)' => 'blob',
  3640. 'bit' => 'boolean',
  3641. 'datetime' => 'timestamp',
  3642. 'datetime2' => 'timestamp',
  3643. 'float' => 'double',
  3644. 'image' => 'blob',
  3645. 'int' => 'integer',
  3646. 'money' => 'decimal',
  3647. 'ntext' => 'clob',
  3648. 'smalldatetime' => 'timestamp',
  3649. 'smallmoney' => 'decimal',
  3650. 'text' => 'clob',
  3651. 'timestamp' => 'binary',
  3652. 'udt' => 'varbinary',
  3653. 'uniqueidentifier' => 'char',
  3654. 'xml' => 'clob',
  3655. ],
  3656. ];
  3657. private $valid = [
  3658. 'bigint' => true,
  3659. 'binary' => true,
  3660. 'bit' => true,
  3661. 'blob' => true,
  3662. 'boolean' => true,
  3663. 'char' => true,
  3664. 'clob' => true,
  3665. 'date' => true,
  3666. 'decimal' => true,
  3667. 'distinct' => true,
  3668. 'double' => true,
  3669. 'float' => true,
  3670. 'integer' => true,
  3671. 'longnvarchar' => true,
  3672. 'longvarbinary' => true,
  3673. 'longvarchar' => true,
  3674. 'nchar' => true,
  3675. 'nclob' => true,
  3676. 'numeric' => true,
  3677. 'nvarchar' => true,
  3678. 'real' => true,
  3679. 'smallint' => true,
  3680. 'time' => true,
  3681. 'time_with_timezone' => true,
  3682. 'timestamp' => true,
  3683. 'timestamp_with_timezone' => true,
  3684. 'tinyint' => true,
  3685. 'varbinary' => true,
  3686. 'varchar' => true,
  3687. 'geometry' => true,
  3688. ];
  3689. public function toJdbc(string $type, int $size): string
  3690. {
  3691. $jdbcType = strtolower($type);
  3692. if (isset($this->toJdbc[$this->driver]["$jdbcType($size)"])) {
  3693. $jdbcType = $this->toJdbc[$this->driver]["$jdbcType($size)"];
  3694. }
  3695. if (isset($this->toJdbc[$this->driver][$jdbcType])) {
  3696. $jdbcType = $this->toJdbc[$this->driver][$jdbcType];
  3697. }
  3698. if (isset($this->toJdbc['simplified'][$jdbcType])) {
  3699. $jdbcType = $this->toJdbc['simplified'][$jdbcType];
  3700. }
  3701. if (!isset($this->valid[$jdbcType])) {
  3702. throw new \Exception("Unsupported type '$jdbcType' for driver '$this->driver'");
  3703. }
  3704. return $jdbcType;
  3705. }
  3706. public function fromJdbc(string $type): string
  3707. {
  3708. $jdbcType = strtolower($type);
  3709. if (isset($this->fromJdbc[$this->driver][$jdbcType])) {
  3710. $jdbcType = $this->fromJdbc[$this->driver][$jdbcType];
  3711. }
  3712. return $jdbcType;
  3713. }
  3714. }
  3715. // file: src/Tqdev/PhpCrudApi/Middleware/Base/Middleware.php
  3716. abstract class Middleware implements MiddlewareInterface
  3717. {
  3718. protected $next;
  3719. protected $responder;
  3720. private $properties;
  3721. public function __construct(Router $router, Responder $responder, array $properties)
  3722. {
  3723. $router->load($this);
  3724. $this->responder = $responder;
  3725. $this->properties = $properties;
  3726. }
  3727. protected function getArrayProperty(string $key, string $default): array
  3728. {
  3729. return array_filter(array_map('trim', explode(',', $this->getProperty($key, $default))));
  3730. }
  3731. protected function getProperty(string $key, $default)
  3732. {
  3733. return isset($this->properties[$key]) ? $this->properties[$key] : $default;
  3734. }
  3735. }
  3736. // file: src/Tqdev/PhpCrudApi/Middleware/Communication/VariableStore.php
  3737. class VariableStore
  3738. {
  3739. static $values = array();
  3740. public static function get(string $key)
  3741. {
  3742. if (isset(self::$values[$key])) {
  3743. return self::$values[$key];
  3744. }
  3745. return null;
  3746. }
  3747. public static function set(string $key, /* object */ $value)
  3748. {
  3749. self::$values[$key] = $value;
  3750. }
  3751. }
  3752. // file: src/Tqdev/PhpCrudApi/Middleware/Router/Router.php
  3753. interface Router extends RequestHandlerInterface
  3754. {
  3755. public function register(string $method, string $path, array $handler);
  3756. public function load(Middleware $middleware);
  3757. public function route(ServerRequestInterface $request): ResponseInterface;
  3758. }
  3759. // file: src/Tqdev/PhpCrudApi/Middleware/Router/SimpleRouter.php
  3760. class SimpleRouter implements Router
  3761. {
  3762. private $responder;
  3763. private $cache;
  3764. private $ttl;
  3765. private $debug;
  3766. private $registration;
  3767. private $routes;
  3768. private $routeHandlers;
  3769. private $middlewares;
  3770. public function __construct(Responder $responder, Cache $cache, int $ttl, bool $debug)
  3771. {
  3772. $this->responder = $responder;
  3773. $this->cache = $cache;
  3774. $this->ttl = $ttl;
  3775. $this->debug = $debug;
  3776. $this->registration = true;
  3777. $this->routes = $this->loadPathTree();
  3778. $this->routeHandlers = [];
  3779. $this->middlewares = array();
  3780. }
  3781. private function loadPathTree(): PathTree
  3782. {
  3783. $data = $this->cache->get('PathTree');
  3784. if ($data != '') {
  3785. $tree = PathTree::fromJson(json_decode(gzuncompress($data)));
  3786. $this->registration = false;
  3787. } else {
  3788. $tree = new PathTree();
  3789. }
  3790. return $tree;
  3791. }
  3792. public function register(string $method, string $path, array $handler)
  3793. {
  3794. $routeNumber = count($this->routeHandlers);
  3795. $this->routeHandlers[$routeNumber] = $handler;
  3796. if ($this->registration) {
  3797. $parts = explode('/', trim($path, '/'));
  3798. array_unshift($parts, $method);
  3799. $this->routes->put($parts, $routeNumber);
  3800. }
  3801. }
  3802. public function load(Middleware $middleware) /*: void*/
  3803. {
  3804. array_push($this->middlewares, $middleware);
  3805. }
  3806. public function route(ServerRequestInterface $request): ResponseInterface
  3807. {
  3808. if ($this->registration) {
  3809. $data = gzcompress(json_encode($this->routes, JSON_UNESCAPED_UNICODE));
  3810. $this->cache->set('PathTree', $data, $this->ttl);
  3811. }
  3812. return $this->handle($request);
  3813. }
  3814. private function getRouteNumbers(ServerRequestInterface $request): array
  3815. {
  3816. $method = strtoupper($request->getMethod());
  3817. $path = explode('/', trim($request->getRequestTarget(), '/'));
  3818. array_unshift($path, $method);
  3819. return $this->routes->match($path);
  3820. }
  3821. public function handle(ServerRequestInterface $request): ResponseInterface
  3822. {
  3823. if (count($this->middlewares)) {
  3824. $handler = array_pop($this->middlewares);
  3825. return $handler->process($request, $this);
  3826. }
  3827. $routeNumbers = $this->getRouteNumbers($request);
  3828. if (count($routeNumbers) == 0) {
  3829. return $this->responder->error(ErrorCode::ROUTE_NOT_FOUND, $request->getRequestTarget());
  3830. }
  3831. try {
  3832. $response = call_user_func($this->routeHandlers[$routeNumbers[0]], $request);
  3833. } catch (\PDOException $e) {
  3834. if (strpos(strtolower($e->getMessage()), 'duplicate') !== false) {
  3835. $response = $this->responder->error(ErrorCode::DUPLICATE_KEY_EXCEPTION, '');
  3836. } elseif (strpos(strtolower($e->getMessage()), 'default value') !== false) {
  3837. $response = $this->responder->error(ErrorCode::DATA_INTEGRITY_VIOLATION, '');
  3838. } elseif (strpos(strtolower($e->getMessage()), 'allow nulls') !== false) {
  3839. $response = $this->responder->error(ErrorCode::DATA_INTEGRITY_VIOLATION, '');
  3840. } elseif (strpos(strtolower($e->getMessage()), 'constraint') !== false) {
  3841. $response = $this->responder->error(ErrorCode::DATA_INTEGRITY_VIOLATION, '');
  3842. }
  3843. if ($this->debug) {
  3844. $response = ResponseUtils::addExceptionHeaders($response, $e);
  3845. }
  3846. }
  3847. return $response;
  3848. }
  3849. }
  3850. // file: src/Tqdev/PhpCrudApi/Middleware/AjaxOnlyMiddleware.php
  3851. class AjaxOnlyMiddleware extends Middleware
  3852. {
  3853. public function process(ServerRequestInterface $request, RequestHandlerInterface $next): ResponseInterface
  3854. {
  3855. $method = $request->getMethod();
  3856. $excludeMethods = $this->getArrayProperty('excludeMethods', 'OPTIONS,GET');
  3857. if (!in_array($method, $excludeMethods)) {
  3858. $headerName = $this->getProperty('headerName', 'X-Requested-With');
  3859. $headerValue = $this->getProperty('headerValue', 'XMLHttpRequest');
  3860. if ($headerValue != RequestUtils::getHeader($request, $headerName)) {
  3861. return $this->responder->error(ErrorCode::ONLY_AJAX_REQUESTS_ALLOWED, $method);
  3862. }
  3863. }
  3864. return $next->handle($request);
  3865. }
  3866. }
  3867. // file: src/Tqdev/PhpCrudApi/Middleware/AuthorizationMiddleware.php
  3868. class AuthorizationMiddleware extends Middleware
  3869. {
  3870. private $reflection;
  3871. public function __construct(Router $router, Responder $responder, array $properties, ReflectionService $reflection)
  3872. {
  3873. parent::__construct($router, $responder, $properties);
  3874. $this->reflection = $reflection;
  3875. }
  3876. private function handleColumns(string $operation, string $tableName) /*: void*/
  3877. {
  3878. $columnHandler = $this->getProperty('columnHandler', '');
  3879. if ($columnHandler) {
  3880. $table = $this->reflection->getTable($tableName);
  3881. foreach ($table->getColumnNames() as $columnName) {
  3882. $allowed = call_user_func($columnHandler, $operation, $tableName, $columnName);
  3883. if (!$allowed) {
  3884. $table->removeColumn($columnName);
  3885. }
  3886. }
  3887. }
  3888. }
  3889. private function handleTable(string $operation, string $tableName) /*: void*/
  3890. {
  3891. if (!$this->reflection->hasTable($tableName)) {
  3892. return;
  3893. }
  3894. $tableHandler = $this->getProperty('tableHandler', '');
  3895. if ($tableHandler) {
  3896. $allowed = call_user_func($tableHandler, $operation, $tableName);
  3897. if (!$allowed) {
  3898. $this->reflection->removeTable($tableName);
  3899. } else {
  3900. $this->handleColumns($operation, $tableName);
  3901. }
  3902. }
  3903. }
  3904. private function handleRecords(string $operation, string $tableName) /*: void*/
  3905. {
  3906. if (!$this->reflection->hasTable($tableName)) {
  3907. return;
  3908. }
  3909. $recordHandler = $this->getProperty('recordHandler', '');
  3910. if ($recordHandler) {
  3911. $query = call_user_func($recordHandler, $operation, $tableName);
  3912. $filters = new FilterInfo();
  3913. $table = $this->reflection->getTable($tableName);
  3914. $query = str_replace('][]=', ']=', str_replace('=', '[]=', $query));
  3915. parse_str($query, $params);
  3916. $condition = $filters->getCombinedConditions($table, $params);
  3917. VariableStore::set("authorization.conditions.$tableName", $condition);
  3918. }
  3919. }
  3920. public function process(ServerRequestInterface $request, RequestHandlerInterface $next): ResponseInterface
  3921. {
  3922. $path = RequestUtils::getPathSegment($request, 1);
  3923. $operation = RequestUtils::getOperation($request);
  3924. $tableNames = RequestUtils::getTableNames($request, $this->reflection);
  3925. foreach ($tableNames as $tableName) {
  3926. $this->handleTable($operation, $tableName);
  3927. if ($path == 'records') {
  3928. $this->handleRecords($operation, $tableName);
  3929. }
  3930. }
  3931. if ($path == 'openapi') {
  3932. VariableStore::set('authorization.tableHandler', $this->getProperty('tableHandler', ''));
  3933. VariableStore::set('authorization.columnHandler', $this->getProperty('columnHandler', ''));
  3934. }
  3935. return $next->handle($request);
  3936. }
  3937. }
  3938. // file: src/Tqdev/PhpCrudApi/Middleware/BasicAuthMiddleware.php
  3939. class BasicAuthMiddleware extends Middleware
  3940. {
  3941. private function hasCorrectPassword(string $username, string $password, array &$passwords): bool
  3942. {
  3943. $hash = isset($passwords[$username]) ? $passwords[$username] : false;
  3944. if ($hash && password_verify($password, $hash)) {
  3945. if (password_needs_rehash($hash, PASSWORD_DEFAULT)) {
  3946. $passwords[$username] = password_hash($password, PASSWORD_DEFAULT);
  3947. }
  3948. return true;
  3949. }
  3950. return false;
  3951. }
  3952. private function getValidUsername(string $username, string $password, string $passwordFile): string
  3953. {
  3954. $passwords = $this->readPasswords($passwordFile);
  3955. $valid = $this->hasCorrectPassword($username, $password, $passwords);
  3956. $this->writePasswords($passwordFile, $passwords);
  3957. return $valid ? $username : '';
  3958. }
  3959. private function readPasswords(string $passwordFile): array
  3960. {
  3961. $passwords = [];
  3962. $passwordLines = file($passwordFile);
  3963. foreach ($passwordLines as $passwordLine) {
  3964. if (strpos($passwordLine, ':') !== false) {
  3965. list($username, $hash) = explode(':', trim($passwordLine), 2);
  3966. if (strlen($hash) > 0 && $hash[0] != '$') {
  3967. $hash = password_hash($hash, PASSWORD_DEFAULT);
  3968. }
  3969. $passwords[$username] = $hash;
  3970. }
  3971. }
  3972. return $passwords;
  3973. }
  3974. private function writePasswords(string $passwordFile, array $passwords): bool
  3975. {
  3976. $success = false;
  3977. $passwordFileContents = '';
  3978. foreach ($passwords as $username => $hash) {
  3979. $passwordFileContents .= "$username:$hash\n";
  3980. }
  3981. if (file_get_contents($passwordFile) != $passwordFileContents) {
  3982. $success = file_put_contents($passwordFile, $passwordFileContents) !== false;
  3983. }
  3984. return $success;
  3985. }
  3986. private function getAuthorizationCredentials(ServerRequestInterface $request): string
  3987. {
  3988. if (isset($_SERVER['PHP_AUTH_USER'])) {
  3989. return $_SERVER['PHP_AUTH_USER'] . ':' . $_SERVER['PHP_AUTH_PW'];
  3990. }
  3991. $header = RequestUtils::getHeader($request, 'Authorization');
  3992. $parts = explode(' ', trim($header), 2);
  3993. if (count($parts) != 2) {
  3994. return '';
  3995. }
  3996. if ($parts[0] != 'Basic') {
  3997. return '';
  3998. }
  3999. return base64_decode(strtr($parts[1], '-_', '+/'));
  4000. }
  4001. public function process(ServerRequestInterface $request, RequestHandlerInterface $next): ResponseInterface
  4002. {
  4003. if (session_status() == PHP_SESSION_NONE) {
  4004. session_start();
  4005. }
  4006. $credentials = $this->getAuthorizationCredentials($request);
  4007. if ($credentials) {
  4008. list($username, $password) = array('', '');
  4009. if (strpos($credentials, ':') !== false) {
  4010. list($username, $password) = explode(':', $credentials, 2);
  4011. }
  4012. $passwordFile = $this->getProperty('passwordFile', '.htpasswd');
  4013. $validUser = $this->getValidUsername($username, $password, $passwordFile);
  4014. $_SESSION['username'] = $validUser;
  4015. if (!$validUser) {
  4016. return $this->responder->error(ErrorCode::AUTHENTICATION_FAILED, $username);
  4017. }
  4018. if (!headers_sent()) {
  4019. session_regenerate_id();
  4020. }
  4021. }
  4022. if (!isset($_SESSION['username']) || !$_SESSION['username']) {
  4023. $authenticationMode = $this->getProperty('mode', 'required');
  4024. if ($authenticationMode == 'required') {
  4025. $response = $this->responder->error(ErrorCode::AUTHENTICATION_REQUIRED, '');
  4026. $realm = $this->getProperty('realm', 'Username and password required');
  4027. $response = $response->withHeader('WWW-Authenticate', "Basic realm=\"$realm\"");
  4028. return $response;
  4029. }
  4030. }
  4031. return $next->handle($request);
  4032. }
  4033. }
  4034. // file: src/Tqdev/PhpCrudApi/Middleware/CorsMiddleware.php
  4035. class CorsMiddleware extends Middleware
  4036. {
  4037. private function isOriginAllowed(string $origin, string $allowedOrigins): bool
  4038. {
  4039. $found = false;
  4040. foreach (explode(',', $allowedOrigins) as $allowedOrigin) {
  4041. $hostname = preg_quote(strtolower(trim($allowedOrigin)));
  4042. $regex = '/^' . str_replace('\*', '.*', $hostname) . '$/';
  4043. if (preg_match($regex, $origin)) {
  4044. $found = true;
  4045. break;
  4046. }
  4047. }
  4048. return $found;
  4049. }
  4050. public function process(ServerRequestInterface $request, RequestHandlerInterface $next): ResponseInterface
  4051. {
  4052. $method = $request->getMethod();
  4053. $origin = count($request->getHeader('Origin')) ? $request->getHeader('Origin')[0] : '';
  4054. $allowedOrigins = $this->getProperty('allowedOrigins', '*');
  4055. if ($origin && !$this->isOriginAllowed($origin, $allowedOrigins)) {
  4056. $response = $this->responder->error(ErrorCode::ORIGIN_FORBIDDEN, $origin);
  4057. } elseif ($method == 'OPTIONS') {
  4058. $response = ResponseFactory::fromStatus(ResponseFactory::OK);
  4059. $allowHeaders = $this->getProperty('allowHeaders', 'Content-Type, X-XSRF-TOKEN, X-Authorization');
  4060. if ($allowHeaders) {
  4061. $response = $response->withHeader('Access-Control-Allow-Headers', $allowHeaders);
  4062. }
  4063. $allowMethods = $this->getProperty('allowMethods', 'OPTIONS, GET, PUT, POST, DELETE, PATCH');
  4064. if ($allowMethods) {
  4065. $response = $response->withHeader('Access-Control-Allow-Methods', $allowMethods);
  4066. }
  4067. $allowCredentials = $this->getProperty('allowCredentials', 'true');
  4068. if ($allowCredentials) {
  4069. $response = $response->withHeader('Access-Control-Allow-Credentials', $allowCredentials);
  4070. }
  4071. $maxAge = $this->getProperty('maxAge', '1728000');
  4072. if ($maxAge) {
  4073. $response = $response->withHeader('Access-Control-Max-Age', $maxAge);
  4074. }
  4075. $exposeHeaders = $this->getProperty('exposeHeaders', '');
  4076. if ($exposeHeaders) {
  4077. $response = $response->withHeader('Access-Control-Expose-Headers', $exposeHeaders);
  4078. }
  4079. } else {
  4080. $response = $next->handle($request);
  4081. }
  4082. if ($origin) {
  4083. $allowCredentials = $this->getProperty('allowCredentials', 'true');
  4084. if ($allowCredentials) {
  4085. $response = $response->withHeader('Access-Control-Allow-Credentials', $allowCredentials);
  4086. }
  4087. $response = $response->withHeader('Access-Control-Allow-Origin', $origin);
  4088. }
  4089. return $response;
  4090. }
  4091. }
  4092. // file: src/Tqdev/PhpCrudApi/Middleware/CustomizationMiddleware.php
  4093. class CustomizationMiddleware extends Middleware
  4094. {
  4095. private $reflection;
  4096. public function __construct(Router $router, Responder $responder, array $properties, ReflectionService $reflection)
  4097. {
  4098. parent::__construct($router, $responder, $properties);
  4099. $this->reflection = $reflection;
  4100. }
  4101. public function process(ServerRequestInterface $request, RequestHandlerInterface $next): ResponseInterface
  4102. {
  4103. $operation = RequestUtils::getOperation($request);
  4104. $tableName = RequestUtils::getPathSegment($request, 2);
  4105. $beforeHandler = $this->getProperty('beforeHandler', '');
  4106. $environment = (object) array();
  4107. if ($beforeHandler !== '') {
  4108. $result = call_user_func($beforeHandler, $operation, $tableName, $request, $environment);
  4109. $request = $result ?: $request;
  4110. }
  4111. $response = $next->handle($request);
  4112. $afterHandler = $this->getProperty('afterHandler', '');
  4113. if ($afterHandler !== '') {
  4114. $result = call_user_func($afterHandler, $operation, $tableName, $response, $environment);
  4115. $response = $result ?: $response;
  4116. }
  4117. return $response;
  4118. }
  4119. }
  4120. // file: src/Tqdev/PhpCrudApi/Middleware/FirewallMiddleware.php
  4121. class FirewallMiddleware extends Middleware
  4122. {
  4123. private function ipMatch(string $ip, string $cidr): bool
  4124. {
  4125. if (strpos($cidr, '/') !== false) {
  4126. list($subnet, $mask) = explode('/', trim($cidr));
  4127. if ((ip2long($ip) & ~((1 << (32 - $mask)) - 1)) == ip2long($subnet)) {
  4128. return true;
  4129. }
  4130. } else {
  4131. if (ip2long($ip) == ip2long($cidr)) {
  4132. return true;
  4133. }
  4134. }
  4135. return false;
  4136. }
  4137. private function isIpAllowed(string $ipAddress, string $allowedIpAddresses): bool
  4138. {
  4139. foreach (explode(',', $allowedIpAddresses) as $allowedIp) {
  4140. if ($this->ipMatch($ipAddress, $allowedIp)) {
  4141. return true;
  4142. }
  4143. }
  4144. return false;
  4145. }
  4146. public function process(ServerRequestInterface $request, RequestHandlerInterface $next): ResponseInterface
  4147. {
  4148. $reverseProxy = $this->getProperty('reverseProxy', '');
  4149. if ($reverseProxy) {
  4150. $ipAddress = array_pop(explode(',', $request->getHeader('X-Forwarded-For')));
  4151. } elseif (isset($_SERVER['REMOTE_ADDR'])) {
  4152. $ipAddress = $_SERVER['REMOTE_ADDR'];
  4153. } else {
  4154. $ipAddress = '127.0.0.1';
  4155. }
  4156. $allowedIpAddresses = $this->getProperty('allowedIpAddresses', '');
  4157. if (!$this->isIpAllowed($ipAddress, $allowedIpAddresses)) {
  4158. $response = $this->responder->error(ErrorCode::TEMPORARY_OR_PERMANENTLY_BLOCKED, '');
  4159. } else {
  4160. $response = $next->handle($request);
  4161. }
  4162. return $response;
  4163. }
  4164. }
  4165. // file: src/Tqdev/PhpCrudApi/Middleware/IpAddressMiddleware.php
  4166. class IpAddressMiddleware extends Middleware
  4167. {
  4168. private $reflection;
  4169. public function __construct(Router $router, Responder $responder, array $properties, ReflectionService $reflection)
  4170. {
  4171. parent::__construct($router, $responder, $properties);
  4172. $this->reflection = $reflection;
  4173. }
  4174. private function callHandler($record, string $operation, ReflectedTable $table) /*: object */
  4175. {
  4176. $context = (array) $record;
  4177. $columnNames = $this->getProperty('columns', '');
  4178. if ($columnNames) {
  4179. foreach (explode(',', $columnNames) as $columnName) {
  4180. if ($table->hasColumn($columnName)) {
  4181. if ($operation == 'create') {
  4182. $context[$columnName] = $_SERVER['REMOTE_ADDR'];
  4183. } else {
  4184. unset($context[$columnName]);
  4185. }
  4186. }
  4187. }
  4188. }
  4189. return (object) $context;
  4190. }
  4191. public function process(ServerRequestInterface $request, RequestHandlerInterface $next): ResponseInterface
  4192. {
  4193. $operation = RequestUtils::getOperation($request);
  4194. if (in_array($operation, ['create', 'update', 'increment'])) {
  4195. $tableNames = $this->getProperty('tables', '');
  4196. $tableName = RequestUtils::getPathSegment($request, 2);
  4197. if (!$tableNames || in_array($tableName, explode(',', $tableNames))) {
  4198. if ($this->reflection->hasTable($tableName)) {
  4199. $record = $request->getParsedBody();
  4200. if ($record !== null) {
  4201. $table = $this->reflection->getTable($tableName);
  4202. if (is_array($record)) {
  4203. foreach ($record as &$r) {
  4204. $r = $this->callHandler($r, $operation, $table);
  4205. }
  4206. } else {
  4207. $record = $this->callHandler($record, $operation, $table);
  4208. }
  4209. $request = $request->withParsedBody($record);
  4210. }
  4211. }
  4212. }
  4213. }
  4214. return $next->handle($request);
  4215. }
  4216. }
  4217. // file: src/Tqdev/PhpCrudApi/Middleware/JoinLimitsMiddleware.php
  4218. class JoinLimitsMiddleware extends Middleware
  4219. {
  4220. private $reflection;
  4221. public function __construct(Router $router, Responder $responder, array $properties, ReflectionService $reflection)
  4222. {
  4223. parent::__construct($router, $responder, $properties);
  4224. $this->reflection = $reflection;
  4225. }
  4226. public function process(ServerRequestInterface $request, RequestHandlerInterface $next): ResponseInterface
  4227. {
  4228. $operation = RequestUtils::getOperation($request);
  4229. $params = RequestUtils::getParams($request);
  4230. if (in_array($operation, ['read', 'list']) && isset($params['join'])) {
  4231. $maxDepth = (int) $this->getProperty('depth', '3');
  4232. $maxTables = (int) $this->getProperty('tables', '10');
  4233. $maxRecords = (int) $this->getProperty('records', '1000');
  4234. $tableCount = 0;
  4235. $joinPaths = array();
  4236. for ($i = 0; $i < count($params['join']); $i++) {
  4237. $joinPath = array();
  4238. $tables = explode(',', $params['join'][$i]);
  4239. for ($depth = 0; $depth < min($maxDepth, count($tables)); $depth++) {
  4240. array_push($joinPath, $tables[$depth]);
  4241. $tableCount += 1;
  4242. if ($tableCount == $maxTables) {
  4243. break;
  4244. }
  4245. }
  4246. array_push($joinPaths, implode(',', $joinPath));
  4247. if ($tableCount == $maxTables) {
  4248. break;
  4249. }
  4250. }
  4251. $params['join'] = $joinPaths;
  4252. $request = RequestUtils::setParams($request, $params);
  4253. VariableStore::set("joinLimits.maxRecords", $maxRecords);
  4254. }
  4255. return $next->handle($request);
  4256. }
  4257. }
  4258. // file: src/Tqdev/PhpCrudApi/Middleware/JwtAuthMiddleware.php
  4259. class JwtAuthMiddleware extends Middleware
  4260. {
  4261. private function getVerifiedClaims(string $token, int $time, int $leeway, int $ttl, string $secret, array $requirements): array
  4262. {
  4263. $algorithms = array(
  4264. 'HS256' => 'sha256',
  4265. 'HS384' => 'sha384',
  4266. 'HS512' => 'sha512',
  4267. 'RS256' => 'sha256',
  4268. 'RS384' => 'sha384',
  4269. 'RS512' => 'sha512',
  4270. );
  4271. $token = explode('.', $token);
  4272. if (count($token) < 3) {
  4273. return array();
  4274. }
  4275. $header = json_decode(base64_decode(strtr($token[0], '-_', '+/')), true);
  4276. if (!$secret) {
  4277. return array();
  4278. }
  4279. if ($header['typ'] != 'JWT') {
  4280. return array();
  4281. }
  4282. $algorithm = $header['alg'];
  4283. if (!isset($algorithms[$algorithm])) {
  4284. return array();
  4285. }
  4286. if (!empty($requirements['alg']) && !in_array($algorithm, $requirements['alg'])) {
  4287. return array();
  4288. }
  4289. $hmac = $algorithms[$algorithm];
  4290. $signature = base64_decode(strtr($token[2], '-_', '+/'));
  4291. $data = "$token[0].$token[1]";
  4292. switch ($algorithm[0]) {
  4293. case 'H':
  4294. $hash = hash_hmac($hmac, $data, $secret, true);
  4295. if (function_exists('hash_equals')) {
  4296. $equals = hash_equals($signature, $hash);
  4297. } else {
  4298. $equals = $signature == $hash;
  4299. }
  4300. if (!$equals) {
  4301. return array();
  4302. }
  4303. break;
  4304. case 'R':
  4305. $equals = openssl_verify($data, $signature, $secret, $hmac) == 1;
  4306. if (!$equals) {
  4307. return array();
  4308. }
  4309. break;
  4310. }
  4311. $claims = json_decode(base64_decode(strtr($token[1], '-_', '+/')), true);
  4312. if (!$claims) {
  4313. return array();
  4314. }
  4315. foreach ($requirements as $field => $values) {
  4316. if (!empty($values)) {
  4317. if ($field != 'alg') {
  4318. if (!isset($claims[$field]) || !in_array($claims[$field], $values)) {
  4319. return array();
  4320. }
  4321. }
  4322. }
  4323. }
  4324. if (isset($claims['nbf']) && $time + $leeway < $claims['nbf']) {
  4325. return array();
  4326. }
  4327. if (isset($claims['iat']) && $time + $leeway < $claims['iat']) {
  4328. return array();
  4329. }
  4330. if (isset($claims['exp']) && $time - $leeway > $claims['exp']) {
  4331. return array();
  4332. }
  4333. if (isset($claims['iat']) && !isset($claims['exp'])) {
  4334. if ($time - $leeway > $claims['iat'] + $ttl) {
  4335. return array();
  4336. }
  4337. }
  4338. return $claims;
  4339. }
  4340. private function getClaims(string $token): array
  4341. {
  4342. $time = (int) $this->getProperty('time', time());
  4343. $leeway = (int) $this->getProperty('leeway', '5');
  4344. $ttl = (int) $this->getProperty('ttl', '30');
  4345. $secret = $this->getProperty('secret', '');
  4346. $requirements = array(
  4347. 'alg' => $this->getArrayProperty('algorithms', ''),
  4348. 'aud' => $this->getArrayProperty('audiences', ''),
  4349. 'iss' => $this->getArrayProperty('issuers', ''),
  4350. );
  4351. if (!$secret) {
  4352. return array();
  4353. }
  4354. return $this->getVerifiedClaims($token, $time, $leeway, $ttl, $secret, $requirements);
  4355. }
  4356. private function getAuthorizationToken(ServerRequestInterface $request): string
  4357. {
  4358. $headerName = $this->getProperty('header', 'X-Authorization');
  4359. $headerValue = RequestUtils::getHeader($request, $headerName);
  4360. $parts = explode(' ', trim($headerValue), 2);
  4361. if (count($parts) != 2) {
  4362. return '';
  4363. }
  4364. if ($parts[0] != 'Bearer') {
  4365. return '';
  4366. }
  4367. return $parts[1];
  4368. }
  4369. public function process(ServerRequestInterface $request, RequestHandlerInterface $next): ResponseInterface
  4370. {
  4371. if (session_status() == PHP_SESSION_NONE) {
  4372. session_start();
  4373. }
  4374. $token = $this->getAuthorizationToken($request);
  4375. if ($token) {
  4376. $claims = $this->getClaims($token);
  4377. $_SESSION['claims'] = $claims;
  4378. if (empty($claims)) {
  4379. return $this->responder->error(ErrorCode::AUTHENTICATION_FAILED, 'JWT');
  4380. }
  4381. if (!headers_sent()) {
  4382. session_regenerate_id();
  4383. }
  4384. }
  4385. if (empty($_SESSION['claims'])) {
  4386. $authenticationMode = $this->getProperty('mode', 'required');
  4387. if ($authenticationMode == 'required') {
  4388. return $this->responder->error(ErrorCode::AUTHENTICATION_REQUIRED, '');
  4389. }
  4390. }
  4391. return $next->handle($request);
  4392. }
  4393. }
  4394. // file: src/Tqdev/PhpCrudApi/Middleware/MultiTenancyMiddleware.php
  4395. class MultiTenancyMiddleware extends Middleware
  4396. {
  4397. private $reflection;
  4398. public function __construct(Router $router, Responder $responder, array $properties, ReflectionService $reflection)
  4399. {
  4400. parent::__construct($router, $responder, $properties);
  4401. $this->reflection = $reflection;
  4402. }
  4403. private function getCondition(string $tableName, array $pairs): Condition
  4404. {
  4405. $condition = new NoCondition();
  4406. $table = $this->reflection->getTable($tableName);
  4407. foreach ($pairs as $k => $v) {
  4408. $condition = $condition->_and(new ColumnCondition($table->getColumn($k), 'eq', $v));
  4409. }
  4410. return $condition;
  4411. }
  4412. private function getPairs($handler, string $operation, string $tableName): array
  4413. {
  4414. $result = array();
  4415. $pairs = call_user_func($handler, $operation, $tableName);
  4416. $table = $this->reflection->getTable($tableName);
  4417. foreach ($pairs as $k => $v) {
  4418. if ($table->hasColumn($k)) {
  4419. $result[$k] = $v;
  4420. }
  4421. }
  4422. return $result;
  4423. }
  4424. private function handleRecord(ServerRequestInterface $request, string $operation, array $pairs): ServerRequestInterface
  4425. {
  4426. $record = $request->getParsedBody();
  4427. if ($record === null) {
  4428. return $request;
  4429. }
  4430. $multi = is_array($record);
  4431. $records = $multi ? $record : [$record];
  4432. foreach ($records as &$record) {
  4433. foreach ($pairs as $column => $value) {
  4434. if ($operation == 'create') {
  4435. $record->$column = $value;
  4436. } else {
  4437. if (isset($record->$column)) {
  4438. unset($record->$column);
  4439. }
  4440. }
  4441. }
  4442. }
  4443. return $request->withParsedBody($multi ? $records : $records[0]);
  4444. }
  4445. public function process(ServerRequestInterface $request, RequestHandlerInterface $next): ResponseInterface
  4446. {
  4447. $handler = $this->getProperty('handler', '');
  4448. if ($handler !== '') {
  4449. $path = RequestUtils::getPathSegment($request, 1);
  4450. if ($path == 'records') {
  4451. $operation = RequestUtils::getOperation($request);
  4452. $tableNames = RequestUtils::getTableNames($request, $this->reflection);
  4453. foreach ($tableNames as $i => $tableName) {
  4454. if (!$this->reflection->hasTable($tableName)) {
  4455. continue;
  4456. }
  4457. $pairs = $this->getPairs($handler, $operation, $tableName);
  4458. if ($i == 0) {
  4459. if (in_array($operation, ['create', 'update', 'increment'])) {
  4460. $request = $this->handleRecord($request, $operation, $pairs);
  4461. }
  4462. }
  4463. $condition = $this->getCondition($tableName, $pairs);
  4464. VariableStore::set("multiTenancy.conditions.$tableName", $condition);
  4465. }
  4466. }
  4467. }
  4468. return $next->handle($request);
  4469. }
  4470. }
  4471. // file: src/Tqdev/PhpCrudApi/Middleware/PageLimitsMiddleware.php
  4472. class PageLimitsMiddleware extends Middleware
  4473. {
  4474. private $reflection;
  4475. public function __construct(Router $router, Responder $responder, array $properties, ReflectionService $reflection)
  4476. {
  4477. parent::__construct($router, $responder, $properties);
  4478. $this->reflection = $reflection;
  4479. }
  4480. public function process(ServerRequestInterface $request, RequestHandlerInterface $next): ResponseInterface
  4481. {
  4482. $operation = RequestUtils::getOperation($request);
  4483. if ($operation == 'list') {
  4484. $params = RequestUtils::getParams($request);
  4485. $maxPage = (int) $this->getProperty('pages', '100');
  4486. if (isset($params['page']) && $params['page'] && $maxPage > 0) {
  4487. if (strpos($params['page'][0], ',') === false) {
  4488. $page = $params['page'][0];
  4489. } else {
  4490. list($page, $size) = explode(',', $params['page'][0], 2);
  4491. }
  4492. if ($page > $maxPage) {
  4493. return $this->responder->error(ErrorCode::PAGINATION_FORBIDDEN, '');
  4494. }
  4495. }
  4496. $maxSize = (int) $this->getProperty('records', '1000');
  4497. if (!isset($params['size']) || !$params['size'] && $maxSize > 0) {
  4498. $params['size'] = array($maxSize);
  4499. } else {
  4500. $params['size'] = array(min($params['size'][0], $maxSize));
  4501. }
  4502. $request = RequestUtils::setParams($request, $params);
  4503. }
  4504. return $next->handle($request);
  4505. }
  4506. }
  4507. // file: src/Tqdev/PhpCrudApi/Middleware/SanitationMiddleware.php
  4508. class SanitationMiddleware extends Middleware
  4509. {
  4510. private $reflection;
  4511. public function __construct(Router $router, Responder $responder, array $properties, ReflectionService $reflection)
  4512. {
  4513. parent::__construct($router, $responder, $properties);
  4514. $this->reflection = $reflection;
  4515. }
  4516. private function callHandler($handler, $record, string $operation, ReflectedTable $table) /*: object */
  4517. {
  4518. $context = (array) $record;
  4519. $tableName = $table->getName();
  4520. foreach ($context as $columnName => &$value) {
  4521. if ($table->hasColumn($columnName)) {
  4522. $column = $table->getColumn($columnName);
  4523. $value = call_user_func($handler, $operation, $tableName, $column->serialize(), $value);
  4524. }
  4525. }
  4526. return (object) $context;
  4527. }
  4528. public function process(ServerRequestInterface $request, RequestHandlerInterface $next): ResponseInterface
  4529. {
  4530. $operation = RequestUtils::getOperation($request);
  4531. if (in_array($operation, ['create', 'update', 'increment'])) {
  4532. $tableName = RequestUtils::getPathSegment($request, 2);
  4533. if ($this->reflection->hasTable($tableName)) {
  4534. $record = $request->getParsedBody();
  4535. if ($record !== null) {
  4536. $handler = $this->getProperty('handler', '');
  4537. if ($handler !== '') {
  4538. $table = $this->reflection->getTable($tableName);
  4539. if (is_array($record)) {
  4540. foreach ($record as &$r) {
  4541. $r = $this->callHandler($handler, $r, $operation, $table);
  4542. }
  4543. } else {
  4544. $record = $this->callHandler($handler, $record, $operation, $table);
  4545. }
  4546. $request = $request->withParsedBody($record);
  4547. }
  4548. }
  4549. }
  4550. }
  4551. return $next->handle($request);
  4552. }
  4553. }
  4554. // file: src/Tqdev/PhpCrudApi/Middleware/ValidationMiddleware.php
  4555. class ValidationMiddleware extends Middleware
  4556. {
  4557. private $reflection;
  4558. public function __construct(Router $router, Responder $responder, array $properties, ReflectionService $reflection)
  4559. {
  4560. parent::__construct($router, $responder, $properties);
  4561. $this->reflection = $reflection;
  4562. }
  4563. private function callHandler($handler, $record, string $operation, ReflectedTable $table) /*: ResponseInterface?*/
  4564. {
  4565. $context = (array) $record;
  4566. $details = array();
  4567. $tableName = $table->getName();
  4568. foreach ($context as $columnName => $value) {
  4569. if ($table->hasColumn($columnName)) {
  4570. $column = $table->getColumn($columnName);
  4571. $valid = call_user_func($handler, $operation, $tableName, $column->serialize(), $value, $context);
  4572. if ($valid !== true && $valid !== '') {
  4573. $details[$columnName] = $valid;
  4574. }
  4575. }
  4576. }
  4577. if (count($details) > 0) {
  4578. return $this->responder->error(ErrorCode::INPUT_VALIDATION_FAILED, $tableName, $details);
  4579. }
  4580. return null;
  4581. }
  4582. public function process(ServerRequestInterface $request, RequestHandlerInterface $next): ResponseInterface
  4583. {
  4584. $operation = RequestUtils::getOperation($request);
  4585. if (in_array($operation, ['create', 'update', 'increment'])) {
  4586. $tableName = RequestUtils::getPathSegment($request, 2);
  4587. if ($this->reflection->hasTable($tableName)) {
  4588. $record = $request->getParsedBody();
  4589. if ($record !== null) {
  4590. $handler = $this->getProperty('handler', '');
  4591. if ($handler !== '') {
  4592. $table = $this->reflection->getTable($tableName);
  4593. if (is_array($record)) {
  4594. foreach ($record as $r) {
  4595. $response = $this->callHandler($handler, $r, $operation, $table);
  4596. if ($response !== null) {
  4597. return $response;
  4598. }
  4599. }
  4600. } else {
  4601. $response = $this->callHandler($handler, $record, $operation, $table);
  4602. if ($response !== null) {
  4603. return $response;
  4604. }
  4605. }
  4606. }
  4607. }
  4608. }
  4609. }
  4610. return $next->handle($request);
  4611. }
  4612. }
  4613. // file: src/Tqdev/PhpCrudApi/Middleware/XsrfMiddleware.php
  4614. class XsrfMiddleware extends Middleware
  4615. {
  4616. private function getToken(): string
  4617. {
  4618. $cookieName = $this->getProperty('cookieName', 'XSRF-TOKEN');
  4619. if (isset($_COOKIE[$cookieName])) {
  4620. $token = $_COOKIE[$cookieName];
  4621. } else {
  4622. $secure = isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] == 'on';
  4623. $token = bin2hex(random_bytes(8));
  4624. if (!headers_sent()) {
  4625. setcookie($cookieName, $token, 0, '', '', $secure);
  4626. }
  4627. }
  4628. return $token;
  4629. }
  4630. public function process(ServerRequestInterface $request, RequestHandlerInterface $next): ResponseInterface
  4631. {
  4632. $token = $this->getToken();
  4633. $method = $request->getMethod();
  4634. $excludeMethods = $this->getArrayProperty('excludeMethods', 'OPTIONS,GET');
  4635. if (!in_array($method, $excludeMethods)) {
  4636. $headerName = $this->getProperty('headerName', 'X-XSRF-TOKEN');
  4637. if ($token != $request->getHeader($headerName)) {
  4638. return $this->responder->error(ErrorCode::BAD_OR_MISSING_XSRF_TOKEN, '');
  4639. }
  4640. }
  4641. return $next->handle($request);
  4642. }
  4643. }
  4644. // file: src/Tqdev/PhpCrudApi/OpenApi/OpenApiBuilder.php
  4645. class OpenApiBuilder
  4646. {
  4647. private $openapi;
  4648. private $reflection;
  4649. private $operations = [
  4650. 'list' => 'get',
  4651. 'create' => 'post',
  4652. 'read' => 'get',
  4653. 'update' => 'put',
  4654. 'delete' => 'delete',
  4655. 'increment' => 'patch',
  4656. ];
  4657. private $types = [
  4658. 'integer' => ['type' => 'integer', 'format' => 'int32'],
  4659. 'bigint' => ['type' => 'integer', 'format' => 'int64'],
  4660. 'varchar' => ['type' => 'string'],
  4661. 'clob' => ['type' => 'string'],
  4662. 'varbinary' => ['type' => 'string', 'format' => 'byte'],
  4663. 'blob' => ['type' => 'string', 'format' => 'byte'],
  4664. 'decimal' => ['type' => 'string'],
  4665. 'float' => ['type' => 'number', 'format' => 'float'],
  4666. 'double' => ['type' => 'number', 'format' => 'double'],
  4667. 'date' => ['type' => 'string', 'format' => 'date'],
  4668. 'time' => ['type' => 'string', 'format' => 'date-time'],
  4669. 'timestamp' => ['type' => 'string', 'format' => 'date-time'],
  4670. 'geometry' => ['type' => 'string'],
  4671. 'boolean' => ['type' => 'boolean'],
  4672. ];
  4673. public function __construct(ReflectionService $reflection, $base)
  4674. {
  4675. $this->reflection = $reflection;
  4676. $this->openapi = new OpenApiDefinition($base);
  4677. }
  4678. private function getServerUrl(): string
  4679. {
  4680. $protocol = @$_SERVER['HTTP_X_FORWARDED_PROTO'] ?: @$_SERVER['REQUEST_SCHEME'] ?: ((isset($_SERVER["HTTPS"]) && $_SERVER["HTTPS"] == "on") ? "https" : "http");
  4681. $port = @intval($_SERVER['HTTP_X_FORWARDED_PORT']) ?: @intval($_SERVER["SERVER_PORT"]) ?: (($protocol === 'https') ? 443 : 80);
  4682. $host = @explode(":", $_SERVER['HTTP_HOST'])[0] ?: @$_SERVER['SERVER_NAME'] ?: @$_SERVER['SERVER_ADDR'];
  4683. $port = ($protocol === 'https' && $port === 443) || ($protocol === 'http' && $port === 80) ? '' : ':' . $port;
  4684. $path = @trim(substr($_SERVER['REQUEST_URI'], 0, strpos($_SERVER['REQUEST_URI'], '/openapi')), '/');
  4685. return sprintf('%s://%s%s/%s', $protocol, $host, $port, $path);
  4686. }
  4687. private function getAllTableReferences(): array
  4688. {
  4689. $tableReferences = array();
  4690. foreach ($this->reflection->getTableNames() as $tableName) {
  4691. $table = $this->reflection->getTable($tableName);
  4692. foreach ($table->getColumnNames() as $columnName) {
  4693. $column = $table->getColumn($columnName);
  4694. $referencedTableName = $column->getFk();
  4695. if ($referencedTableName) {
  4696. if (!isset($tableReferences[$referencedTableName])) {
  4697. $tableReferences[$referencedTableName] = array();
  4698. }
  4699. $tableReferences[$referencedTableName][] = "$tableName.$columnName";
  4700. }
  4701. }
  4702. }
  4703. return $tableReferences;
  4704. }
  4705. public function build(): OpenApiDefinition
  4706. {
  4707. $this->openapi->set("openapi", "3.0.0");
  4708. if (!$this->openapi->has("servers") && isset($_SERVER['REQUEST_URI'])) {
  4709. $this->openapi->set("servers|0|url", $this->getServerUrl());
  4710. }
  4711. $tableNames = $this->reflection->getTableNames();
  4712. foreach ($tableNames as $tableName) {
  4713. $this->setPath($tableName);
  4714. }
  4715. $this->openapi->set("components|responses|pk_integer|description", "inserted primary key value (integer)");
  4716. $this->openapi->set("components|responses|pk_integer|content|application/json|schema|type", "integer");
  4717. $this->openapi->set("components|responses|pk_integer|content|application/json|schema|format", "int64");
  4718. $this->openapi->set("components|responses|pk_string|description", "inserted primary key value (string)");
  4719. $this->openapi->set("components|responses|pk_string|content|application/json|schema|type", "string");
  4720. $this->openapi->set("components|responses|pk_string|content|application/json|schema|format", "uuid");
  4721. $this->openapi->set("components|responses|rows_affected|description", "number of rows affected (integer)");
  4722. $this->openapi->set("components|responses|rows_affected|content|application/json|schema|type", "integer");
  4723. $this->openapi->set("components|responses|rows_affected|content|application/json|schema|format", "int64");
  4724. $tableReferences = $this->getAllTableReferences();
  4725. foreach ($tableNames as $tableName) {
  4726. $references = isset($tableReferences[$tableName]) ? $tableReferences[$tableName] : array();
  4727. $this->setComponentSchema($tableName, $references);
  4728. $this->setComponentResponse($tableName);
  4729. $this->setComponentRequestBody($tableName);
  4730. }
  4731. $this->setComponentParameters();
  4732. foreach ($tableNames as $index => $tableName) {
  4733. $this->setTag($index, $tableName);
  4734. }
  4735. return $this->openapi;
  4736. }
  4737. private function isOperationOnTableAllowed(string $operation, string $tableName): bool
  4738. {
  4739. $tableHandler = VariableStore::get('authorization.tableHandler');
  4740. if (!$tableHandler) {
  4741. return true;
  4742. }
  4743. return (bool) call_user_func($tableHandler, $operation, $tableName);
  4744. }
  4745. private function isOperationOnColumnAllowed(string $operation, string $tableName, string $columnName): bool
  4746. {
  4747. $columnHandler = VariableStore::get('authorization.columnHandler');
  4748. if (!$columnHandler) {
  4749. return true;
  4750. }
  4751. return (bool) call_user_func($columnHandler, $operation, $tableName, $columnName);
  4752. }
  4753. private function setPath(string $tableName) /*: void*/
  4754. {
  4755. $table = $this->reflection->getTable($tableName);
  4756. $type = $table->getType();
  4757. $pk = $table->getPk();
  4758. $pkName = $pk ? $pk->getName() : '';
  4759. foreach ($this->operations as $operation => $method) {
  4760. if (!$pkName && $operation != 'list') {
  4761. continue;
  4762. }
  4763. if ($type != 'table' && $operation != 'list') {
  4764. continue;
  4765. }
  4766. if (!$this->isOperationOnTableAllowed($operation, $tableName)) {
  4767. continue;
  4768. }
  4769. $parameters = [];
  4770. if (in_array($operation, ['list', 'create'])) {
  4771. $path = sprintf('/records/%s', $tableName);
  4772. if ($operation == 'list') {
  4773. $parameters = ['filter', 'include', 'exclude', 'order', 'size', 'page', 'join'];
  4774. }
  4775. } else {
  4776. $path = sprintf('/records/%s/{%s}', $tableName, $pkName);
  4777. if ($operation == 'read') {
  4778. $parameters = ['pk', 'include', 'exclude', 'join'];
  4779. } else {
  4780. $parameters = ['pk'];
  4781. }
  4782. }
  4783. foreach ($parameters as $p => $parameter) {
  4784. $this->openapi->set("paths|$path|$method|parameters|$p|\$ref", "#/components/parameters/$parameter");
  4785. }
  4786. if (in_array($operation, ['create', 'update', 'increment'])) {
  4787. $this->openapi->set("paths|$path|$method|requestBody|\$ref", "#/components/requestBodies/$operation-" . urlencode($tableName));
  4788. }
  4789. $this->openapi->set("paths|$path|$method|tags|0", "$tableName");
  4790. $this->openapi->set("paths|$path|$method|description", "$operation $tableName");
  4791. switch ($operation) {
  4792. case 'list':
  4793. $this->openapi->set("paths|$path|$method|responses|200|\$ref", "#/components/responses/$operation-" . urlencode($tableName));
  4794. break;
  4795. case 'create':
  4796. if ($pk->getType() == 'integer') {
  4797. $this->openapi->set("paths|$path|$method|responses|200|\$ref", "#/components/responses/pk_integer");
  4798. } else {
  4799. $this->openapi->set("paths|$path|$method|responses|200|\$ref", "#/components/responses/pk_string");
  4800. }
  4801. break;
  4802. case 'read':
  4803. $this->openapi->set("paths|$path|$method|responses|200|\$ref", "#/components/responses/$operation-" . urlencode($tableName));
  4804. break;
  4805. case 'update':
  4806. case 'delete':
  4807. case 'increment':
  4808. $this->openapi->set("paths|$path|$method|responses|200|\$ref", "#/components/responses/rows_affected");
  4809. break;
  4810. }
  4811. }
  4812. }
  4813. private function setComponentSchema(string $tableName, array $references) /*: void*/
  4814. {
  4815. $table = $this->reflection->getTable($tableName);
  4816. $type = $table->getType();
  4817. $pk = $table->getPk();
  4818. $pkName = $pk ? $pk->getName() : '';
  4819. foreach ($this->operations as $operation => $method) {
  4820. if (!$pkName && $operation != 'list') {
  4821. continue;
  4822. }
  4823. if ($type != 'table' && $operation != 'list') {
  4824. continue;
  4825. }
  4826. if ($operation == 'delete') {
  4827. continue;
  4828. }
  4829. if (!$this->isOperationOnTableAllowed($operation, $tableName)) {
  4830. continue;
  4831. }
  4832. if ($operation == 'list') {
  4833. $this->openapi->set("components|schemas|$operation-$tableName|type", "object");
  4834. $this->openapi->set("components|schemas|$operation-$tableName|properties|results|type", "integer");
  4835. $this->openapi->set("components|schemas|$operation-$tableName|properties|results|format", "int64");
  4836. $this->openapi->set("components|schemas|$operation-$tableName|properties|records|type", "array");
  4837. $prefix = "components|schemas|$operation-$tableName|properties|records|items";
  4838. } else {
  4839. $prefix = "components|schemas|$operation-$tableName";
  4840. }
  4841. $this->openapi->set("$prefix|type", "object");
  4842. foreach ($table->getColumnNames() as $columnName) {
  4843. if (!$this->isOperationOnColumnAllowed($operation, $tableName, $columnName)) {
  4844. continue;
  4845. }
  4846. $column = $table->getColumn($columnName);
  4847. $properties = $this->types[$column->getType()];
  4848. foreach ($properties as $key => $value) {
  4849. $this->openapi->set("$prefix|properties|$columnName|$key", $value);
  4850. }
  4851. if ($column->getPk()) {
  4852. $this->openapi->set("$prefix|properties|$columnName|x-primary-key", true);
  4853. $this->openapi->set("$prefix|properties|$columnName|x-referenced", $references);
  4854. }
  4855. $fk = $column->getFk();
  4856. if ($fk) {
  4857. $this->openapi->set("$prefix|properties|$columnName|x-references", $fk);
  4858. }
  4859. }
  4860. }
  4861. }
  4862. private function setComponentResponse(string $tableName) /*: void*/
  4863. {
  4864. $table = $this->reflection->getTable($tableName);
  4865. $type = $table->getType();
  4866. $pk = $table->getPk();
  4867. $pkName = $pk ? $pk->getName() : '';
  4868. foreach (['list', 'read'] as $operation) {
  4869. if (!$pkName && $operation != 'list') {
  4870. continue;
  4871. }
  4872. if ($type != 'table' && $operation != 'list') {
  4873. continue;
  4874. }
  4875. if (!$this->isOperationOnTableAllowed($operation, $tableName)) {
  4876. continue;
  4877. }
  4878. if ($operation == 'list') {
  4879. $this->openapi->set("components|responses|$operation-$tableName|description", "list of $tableName records");
  4880. } else {
  4881. $this->openapi->set("components|responses|$operation-$tableName|description", "single $tableName record");
  4882. }
  4883. $this->openapi->set("components|responses|$operation-$tableName|content|application/json|schema|\$ref", "#/components/schemas/$operation-" . urlencode($tableName));
  4884. }
  4885. }
  4886. private function setComponentRequestBody(string $tableName) /*: void*/
  4887. {
  4888. $table = $this->reflection->getTable($tableName);
  4889. $type = $table->getType();
  4890. $pk = $table->getPk();
  4891. $pkName = $pk ? $pk->getName() : '';
  4892. if ($pkName && $type == 'table') {
  4893. foreach (['create', 'update', 'increment'] as $operation) {
  4894. if (!$this->isOperationOnTableAllowed($operation, $tableName)) {
  4895. continue;
  4896. }
  4897. $this->openapi->set("components|requestBodies|$operation-$tableName|description", "single $tableName record");
  4898. $this->openapi->set("components|requestBodies|$operation-$tableName|content|application/json|schema|\$ref", "#/components/schemas/$operation-" . urlencode($tableName));
  4899. }
  4900. }
  4901. }
  4902. private function setComponentParameters() /*: void*/
  4903. {
  4904. $this->openapi->set("components|parameters|pk|name", "id");
  4905. $this->openapi->set("components|parameters|pk|in", "path");
  4906. $this->openapi->set("components|parameters|pk|schema|type", "string");
  4907. $this->openapi->set("components|parameters|pk|description", "primary key value");
  4908. $this->openapi->set("components|parameters|pk|required", true);
  4909. $this->openapi->set("components|parameters|filter|name", "filter");
  4910. $this->openapi->set("components|parameters|filter|in", "query");
  4911. $this->openapi->set("components|parameters|filter|schema|type", "array");
  4912. $this->openapi->set("components|parameters|filter|schema|items|type", "string");
  4913. $this->openapi->set("components|parameters|filter|description", "Filters to be applied. Each filter consists of a column, an operator and a value (comma separated). Example: id,eq,1");
  4914. $this->openapi->set("components|parameters|filter|required", false);
  4915. $this->openapi->set("components|parameters|include|name", "include");
  4916. $this->openapi->set("components|parameters|include|in", "query");
  4917. $this->openapi->set("components|parameters|include|schema|type", "string");
  4918. $this->openapi->set("components|parameters|include|description", "Columns you want to include in the output (comma separated). Example: posts.*,categories.name");
  4919. $this->openapi->set("components|parameters|include|required", false);
  4920. $this->openapi->set("components|parameters|exclude|name", "exclude");
  4921. $this->openapi->set("components|parameters|exclude|in", "query");
  4922. $this->openapi->set("components|parameters|exclude|schema|type", "string");
  4923. $this->openapi->set("components|parameters|exclude|description", "Columns you want to exclude from the output (comma separated). Example: posts.content");
  4924. $this->openapi->set("components|parameters|exclude|required", false);
  4925. $this->openapi->set("components|parameters|order|name", "order");
  4926. $this->openapi->set("components|parameters|order|in", "query");
  4927. $this->openapi->set("components|parameters|order|schema|type", "array");
  4928. $this->openapi->set("components|parameters|order|schema|items|type", "string");
  4929. $this->openapi->set("components|parameters|order|description", "Column you want to sort on and the sort direction (comma separated). Example: id,desc");
  4930. $this->openapi->set("components|parameters|order|required", false);
  4931. $this->openapi->set("components|parameters|size|name", "size");
  4932. $this->openapi->set("components|parameters|size|in", "query");
  4933. $this->openapi->set("components|parameters|size|schema|type", "string");
  4934. $this->openapi->set("components|parameters|size|description", "Maximum number of results (for top lists). Example: 10");
  4935. $this->openapi->set("components|parameters|size|required", false);
  4936. $this->openapi->set("components|parameters|page|name", "page");
  4937. $this->openapi->set("components|parameters|page|in", "query");
  4938. $this->openapi->set("components|parameters|page|schema|type", "string");
  4939. $this->openapi->set("components|parameters|page|description", "Page number and page size (comma separated). Example: 1,10");
  4940. $this->openapi->set("components|parameters|page|required", false);
  4941. $this->openapi->set("components|parameters|join|name", "join");
  4942. $this->openapi->set("components|parameters|join|in", "query");
  4943. $this->openapi->set("components|parameters|join|schema|type", "array");
  4944. $this->openapi->set("components|parameters|join|schema|items|type", "string");
  4945. $this->openapi->set("components|parameters|join|description", "Paths (comma separated) to related entities that you want to include. Example: comments,users");
  4946. $this->openapi->set("components|parameters|join|required", false);
  4947. }
  4948. private function setTag(int $index, string $tableName) /*: void*/
  4949. {
  4950. $this->openapi->set("tags|$index|name", "$tableName");
  4951. $this->openapi->set("tags|$index|description", "$tableName operations");
  4952. }
  4953. }
  4954. // file: src/Tqdev/PhpCrudApi/OpenApi/OpenApiDefinition.php
  4955. class OpenApiDefinition implements \JsonSerializable
  4956. {
  4957. private $root;
  4958. public function __construct($base)
  4959. {
  4960. $this->root = $base;
  4961. }
  4962. public function set(string $path, $value) /*: void*/
  4963. {
  4964. $parts = explode('|', trim($path, '|'));
  4965. $current = &$this->root;
  4966. while (count($parts) > 0) {
  4967. $part = array_shift($parts);
  4968. if (!isset($current[$part])) {
  4969. $current[$part] = [];
  4970. }
  4971. $current = &$current[$part];
  4972. }
  4973. $current = $value;
  4974. }
  4975. public function has(string $path): bool
  4976. {
  4977. $parts = explode('|', trim($path, '|'));
  4978. $current = &$this->root;
  4979. while (count($parts) > 0) {
  4980. $part = array_shift($parts);
  4981. if (!isset($current[$part])) {
  4982. return false;
  4983. }
  4984. $current = &$current[$part];
  4985. }
  4986. return true;
  4987. }
  4988. public function jsonSerialize()
  4989. {
  4990. return $this->root;
  4991. }
  4992. }
  4993. // file: src/Tqdev/PhpCrudApi/OpenApi/OpenApiService.php
  4994. class OpenApiService
  4995. {
  4996. private $builder;
  4997. public function __construct(ReflectionService $reflection, array $base)
  4998. {
  4999. $this->builder = new OpenApiBuilder($reflection, $base);
  5000. }
  5001. public function get(): OpenApiDefinition
  5002. {
  5003. return $this->builder->build();
  5004. }
  5005. }
  5006. // file: src/Tqdev/PhpCrudApi/Record/Condition/AndCondition.php
  5007. class AndCondition extends Condition
  5008. {
  5009. private $conditions;
  5010. public function __construct(Condition $condition1, Condition $condition2)
  5011. {
  5012. $this->conditions = [$condition1, $condition2];
  5013. }
  5014. public function _and(Condition $condition): Condition
  5015. {
  5016. if ($condition instanceof NoCondition) {
  5017. return $this;
  5018. }
  5019. $this->conditions[] = $condition;
  5020. return $this;
  5021. }
  5022. public function getConditions(): array
  5023. {
  5024. return $this->conditions;
  5025. }
  5026. public static function fromArray(array $conditions): Condition
  5027. {
  5028. $condition = new NoCondition();
  5029. foreach ($conditions as $c) {
  5030. $condition = $condition->_and($c);
  5031. }
  5032. return $condition;
  5033. }
  5034. }
  5035. // file: src/Tqdev/PhpCrudApi/Record/Condition/ColumnCondition.php
  5036. class ColumnCondition extends Condition
  5037. {
  5038. private $column;
  5039. private $operator;
  5040. private $value;
  5041. public function __construct(ReflectedColumn $column, string $operator, string $value)
  5042. {
  5043. $this->column = $column;
  5044. $this->operator = $operator;
  5045. $this->value = $value;
  5046. }
  5047. public function getColumn(): ReflectedColumn
  5048. {
  5049. return $this->column;
  5050. }
  5051. public function getOperator(): string
  5052. {
  5053. return $this->operator;
  5054. }
  5055. public function getValue(): string
  5056. {
  5057. return $this->value;
  5058. }
  5059. }
  5060. // file: src/Tqdev/PhpCrudApi/Record/Condition/Condition.php
  5061. abstract class Condition
  5062. {
  5063. public function _and(Condition $condition): Condition
  5064. {
  5065. if ($condition instanceof NoCondition) {
  5066. return $this;
  5067. }
  5068. return new AndCondition($this, $condition);
  5069. }
  5070. public function _or(Condition $condition): Condition
  5071. {
  5072. if ($condition instanceof NoCondition) {
  5073. return $this;
  5074. }
  5075. return new OrCondition($this, $condition);
  5076. }
  5077. public function _not(): Condition
  5078. {
  5079. return new NotCondition($this);
  5080. }
  5081. public static function fromString(ReflectedTable $table, string $value): Condition
  5082. {
  5083. $condition = new NoCondition();
  5084. $parts = explode(',', $value, 3);
  5085. if (count($parts) < 2) {
  5086. return $condition;
  5087. }
  5088. if (count($parts) < 3) {
  5089. $parts[2] = '';
  5090. }
  5091. $field = $table->getColumn($parts[0]);
  5092. $command = $parts[1];
  5093. $negate = false;
  5094. $spatial = false;
  5095. if (strlen($command) > 2) {
  5096. if (substr($command, 0, 1) == 'n') {
  5097. $negate = true;
  5098. $command = substr($command, 1);
  5099. }
  5100. if (substr($command, 0, 1) == 's') {
  5101. $spatial = true;
  5102. $command = substr($command, 1);
  5103. }
  5104. }
  5105. if ($spatial) {
  5106. if (in_array($command, ['co', 'cr', 'di', 'eq', 'in', 'ov', 'to', 'wi', 'ic', 'is', 'iv'])) {
  5107. $condition = new SpatialCondition($field, $command, $parts[2]);
  5108. }
  5109. } else {
  5110. if (in_array($command, ['cs', 'sw', 'ew', 'eq', 'lt', 'le', 'ge', 'gt', 'bt', 'in', 'is'])) {
  5111. $condition = new ColumnCondition($field, $command, $parts[2]);
  5112. }
  5113. }
  5114. if ($negate) {
  5115. $condition = $condition->_not();
  5116. }
  5117. return $condition;
  5118. }
  5119. }
  5120. // file: src/Tqdev/PhpCrudApi/Record/Condition/NoCondition.php
  5121. class NoCondition extends Condition
  5122. {
  5123. public function _and(Condition $condition): Condition
  5124. {
  5125. return $condition;
  5126. }
  5127. public function _or(Condition $condition): Condition
  5128. {
  5129. return $condition;
  5130. }
  5131. public function _not(): Condition
  5132. {
  5133. return $this;
  5134. }
  5135. }
  5136. // file: src/Tqdev/PhpCrudApi/Record/Condition/NotCondition.php
  5137. class NotCondition extends Condition
  5138. {
  5139. private $condition;
  5140. public function __construct(Condition $condition)
  5141. {
  5142. $this->condition = $condition;
  5143. }
  5144. public function getCondition(): Condition
  5145. {
  5146. return $this->condition;
  5147. }
  5148. }
  5149. // file: src/Tqdev/PhpCrudApi/Record/Condition/OrCondition.php
  5150. class OrCondition extends Condition
  5151. {
  5152. private $conditions;
  5153. public function __construct(Condition $condition1, Condition $condition2)
  5154. {
  5155. $this->conditions = [$condition1, $condition2];
  5156. }
  5157. public function _or(Condition $condition): Condition
  5158. {
  5159. if ($condition instanceof NoCondition) {
  5160. return $this;
  5161. }
  5162. $this->conditions[] = $condition;
  5163. return $this;
  5164. }
  5165. public function getConditions(): array
  5166. {
  5167. return $this->conditions;
  5168. }
  5169. public static function fromArray(array $conditions): Condition
  5170. {
  5171. $condition = new NoCondition();
  5172. foreach ($conditions as $c) {
  5173. $condition = $condition->_or($c);
  5174. }
  5175. return $condition;
  5176. }
  5177. }
  5178. // file: src/Tqdev/PhpCrudApi/Record/Condition/SpatialCondition.php
  5179. class SpatialCondition extends ColumnCondition
  5180. {
  5181. }
  5182. // file: src/Tqdev/PhpCrudApi/Record/Document/ErrorDocument.php
  5183. class ErrorDocument implements \JsonSerializable
  5184. {
  5185. public $code;
  5186. public $message;
  5187. public $details;
  5188. public function __construct(ErrorCode $errorCode, string $argument, $details)
  5189. {
  5190. $this->code = $errorCode->getCode();
  5191. $this->message = $errorCode->getMessage($argument);
  5192. $this->details = $details;
  5193. }
  5194. public function getCode(): int
  5195. {
  5196. return $this->code;
  5197. }
  5198. public function getMessage(): string
  5199. {
  5200. return $this->message;
  5201. }
  5202. public function serialize()
  5203. {
  5204. return [
  5205. 'code' => $this->code,
  5206. 'message' => $this->message,
  5207. 'details' => $this->details,
  5208. ];
  5209. }
  5210. public function jsonSerialize()
  5211. {
  5212. return array_filter($this->serialize());
  5213. }
  5214. }
  5215. // file: src/Tqdev/PhpCrudApi/Record/Document/ListDocument.php
  5216. class ListDocument implements \JsonSerializable
  5217. {
  5218. private $records;
  5219. private $results;
  5220. public function __construct(array $records, int $results)
  5221. {
  5222. $this->records = $records;
  5223. $this->results = $results;
  5224. }
  5225. public function getRecords(): array
  5226. {
  5227. return $this->records;
  5228. }
  5229. public function getResults(): int
  5230. {
  5231. return $this->results;
  5232. }
  5233. public function serialize()
  5234. {
  5235. return [
  5236. 'records' => $this->records,
  5237. 'results' => $this->results,
  5238. ];
  5239. }
  5240. public function jsonSerialize()
  5241. {
  5242. return array_filter($this->serialize(), function ($v) {
  5243. return $v !== 0;
  5244. });
  5245. }
  5246. }
  5247. // file: src/Tqdev/PhpCrudApi/Record/ColumnIncluder.php
  5248. class ColumnIncluder
  5249. {
  5250. private function isMandatory(string $tableName, string $columnName, array $params): bool
  5251. {
  5252. return isset($params['mandatory']) && in_array($tableName . "." . $columnName, $params['mandatory']);
  5253. }
  5254. private function select(string $tableName, bool $primaryTable, array $params, string $paramName,
  5255. array $columnNames, bool $include): array{
  5256. if (!isset($params[$paramName])) {
  5257. return $columnNames;
  5258. }
  5259. $columns = array();
  5260. foreach (explode(',', $params[$paramName][0]) as $columnName) {
  5261. $columns[$columnName] = true;
  5262. }
  5263. $result = array();
  5264. foreach ($columnNames as $columnName) {
  5265. $match = isset($columns['*.*']);
  5266. if (!$match) {
  5267. $match = isset($columns[$tableName . '.*']) || isset($columns[$tableName . '.' . $columnName]);
  5268. }
  5269. if ($primaryTable && !$match) {
  5270. $match = isset($columns['*']) || isset($columns[$columnName]);
  5271. }
  5272. if ($match) {
  5273. if ($include || $this->isMandatory($tableName, $columnName, $params)) {
  5274. $result[] = $columnName;
  5275. }
  5276. } else {
  5277. if (!$include || $this->isMandatory($tableName, $columnName, $params)) {
  5278. $result[] = $columnName;
  5279. }
  5280. }
  5281. }
  5282. return $result;
  5283. }
  5284. public function getNames(ReflectedTable $table, bool $primaryTable, array $params): array
  5285. {
  5286. $tableName = $table->getName();
  5287. $results = $table->getColumnNames();
  5288. $results = $this->select($tableName, $primaryTable, $params, 'include', $results, true);
  5289. $results = $this->select($tableName, $primaryTable, $params, 'exclude', $results, false);
  5290. return $results;
  5291. }
  5292. public function getValues(ReflectedTable $table, bool $primaryTable, /* object */ $record, array $params): array
  5293. {
  5294. $results = array();
  5295. $columnNames = $this->getNames($table, $primaryTable, $params);
  5296. foreach ($columnNames as $columnName) {
  5297. if (property_exists($record, $columnName)) {
  5298. $results[$columnName] = $record->$columnName;
  5299. }
  5300. }
  5301. return $results;
  5302. }
  5303. }
  5304. // file: src/Tqdev/PhpCrudApi/Record/ErrorCode.php
  5305. class ErrorCode
  5306. {
  5307. private $code;
  5308. private $message;
  5309. private $status;
  5310. const ERROR_NOT_FOUND = 9999;
  5311. const ROUTE_NOT_FOUND = 1000;
  5312. const TABLE_NOT_FOUND = 1001;
  5313. const ARGUMENT_COUNT_MISMATCH = 1002;
  5314. const RECORD_NOT_FOUND = 1003;
  5315. const ORIGIN_FORBIDDEN = 1004;
  5316. const COLUMN_NOT_FOUND = 1005;
  5317. const TABLE_ALREADY_EXISTS = 1006;
  5318. const COLUMN_ALREADY_EXISTS = 1007;
  5319. const HTTP_MESSAGE_NOT_READABLE = 1008;
  5320. const DUPLICATE_KEY_EXCEPTION = 1009;
  5321. const DATA_INTEGRITY_VIOLATION = 1010;
  5322. const AUTHENTICATION_REQUIRED = 1011;
  5323. const AUTHENTICATION_FAILED = 1012;
  5324. const INPUT_VALIDATION_FAILED = 1013;
  5325. const OPERATION_FORBIDDEN = 1014;
  5326. const OPERATION_NOT_SUPPORTED = 1015;
  5327. const TEMPORARY_OR_PERMANENTLY_BLOCKED = 1016;
  5328. const BAD_OR_MISSING_XSRF_TOKEN = 1017;
  5329. const ONLY_AJAX_REQUESTS_ALLOWED = 1018;
  5330. const PAGINATION_FORBIDDEN = 1019;
  5331. private $values = [
  5332. 9999 => ["%s", ResponseFactory::INTERNAL_SERVER_ERROR],
  5333. 1000 => ["Route '%s' not found", ResponseFactory::NOT_FOUND],
  5334. 1001 => ["Table '%s' not found", ResponseFactory::NOT_FOUND],
  5335. 1002 => ["Argument count mismatch in '%s'", ResponseFactory::UNPROCESSABLE_ENTITY],
  5336. 1003 => ["Record '%s' not found", ResponseFactory::NOT_FOUND],
  5337. 1004 => ["Origin '%s' is forbidden", ResponseFactory::FORBIDDEN],
  5338. 1005 => ["Column '%s' not found", ResponseFactory::NOT_FOUND],
  5339. 1006 => ["Table '%s' already exists", ResponseFactory::CONFLICT],
  5340. 1007 => ["Column '%s' already exists", ResponseFactory::CONFLICT],
  5341. 1008 => ["Cannot read HTTP message", ResponseFactory::UNPROCESSABLE_ENTITY],
  5342. 1009 => ["Duplicate key exception", ResponseFactory::CONFLICT],
  5343. 1010 => ["Data integrity violation", ResponseFactory::CONFLICT],
  5344. 1011 => ["Authentication required", ResponseFactory::UNAUTHORIZED],
  5345. 1012 => ["Authentication failed for '%s'", ResponseFactory::FORBIDDEN],
  5346. 1013 => ["Input validation failed for '%s'", ResponseFactory::UNPROCESSABLE_ENTITY],
  5347. 1014 => ["Operation forbidden", ResponseFactory::FORBIDDEN],
  5348. 1015 => ["Operation '%s' not supported", ResponseFactory::METHOD_NOT_ALLOWED],
  5349. 1016 => ["Temporary or permanently blocked", ResponseFactory::FORBIDDEN],
  5350. 1017 => ["Bad or missing XSRF token", ResponseFactory::FORBIDDEN],
  5351. 1018 => ["Only AJAX requests allowed for '%s'", ResponseFactory::FORBIDDEN],
  5352. 1019 => ["Pagination forbidden", ResponseFactory::FORBIDDEN],
  5353. ];
  5354. public function __construct(int $code)
  5355. {
  5356. if (!isset($this->values[$code])) {
  5357. $code = 9999;
  5358. }
  5359. $this->code = $code;
  5360. $this->message = $this->values[$code][0];
  5361. $this->status = $this->values[$code][1];
  5362. }
  5363. public function getCode(): int
  5364. {
  5365. return $this->code;
  5366. }
  5367. public function getMessage(string $argument): string
  5368. {
  5369. return sprintf($this->message, $argument);
  5370. }
  5371. public function getStatus(): int
  5372. {
  5373. return $this->status;
  5374. }
  5375. }
  5376. // file: src/Tqdev/PhpCrudApi/Record/FilterInfo.php
  5377. class FilterInfo
  5378. {
  5379. private function addConditionFromFilterPath(PathTree $conditions, array $path, ReflectedTable $table, array $params)
  5380. {
  5381. $key = 'filter' . implode('', $path);
  5382. if (isset($params[$key])) {
  5383. foreach ($params[$key] as $filter) {
  5384. $condition = Condition::fromString($table, $filter);
  5385. if (($condition instanceof NoCondition) == false) {
  5386. $conditions->put($path, $condition);
  5387. }
  5388. }
  5389. }
  5390. }
  5391. private function getConditionsAsPathTree(ReflectedTable $table, array $params): PathTree
  5392. {
  5393. $conditions = new PathTree();
  5394. $this->addConditionFromFilterPath($conditions, [], $table, $params);
  5395. for ($n = ord('0'); $n <= ord('9'); $n++) {
  5396. $this->addConditionFromFilterPath($conditions, [chr($n)], $table, $params);
  5397. for ($l = ord('a'); $l <= ord('f'); $l++) {
  5398. $this->addConditionFromFilterPath($conditions, [chr($n), chr($l)], $table, $params);
  5399. }
  5400. }
  5401. return $conditions;
  5402. }
  5403. private function combinePathTreeOfConditions(PathTree $tree): Condition
  5404. {
  5405. $andConditions = $tree->getValues();
  5406. $and = AndCondition::fromArray($andConditions);
  5407. $orConditions = [];
  5408. foreach ($tree->getKeys() as $p) {
  5409. $orConditions[] = $this->combinePathTreeOfConditions($tree->get($p));
  5410. }
  5411. $or = OrCondition::fromArray($orConditions);
  5412. return $and->_and($or);
  5413. }
  5414. public function getCombinedConditions(ReflectedTable $table, array $params): Condition
  5415. {
  5416. return $this->combinePathTreeOfConditions($this->getConditionsAsPathTree($table, $params));
  5417. }
  5418. }
  5419. // file: src/Tqdev/PhpCrudApi/Record/HabtmValues.php
  5420. class HabtmValues
  5421. {
  5422. public $pkValues;
  5423. public $fkValues;
  5424. public function __construct(array $pkValues, array $fkValues)
  5425. {
  5426. $this->pkValues = $pkValues;
  5427. $this->fkValues = $fkValues;
  5428. }
  5429. }
  5430. // file: src/Tqdev/PhpCrudApi/Record/OrderingInfo.php
  5431. class OrderingInfo
  5432. {
  5433. public function getColumnOrdering(ReflectedTable $table, array $params): array
  5434. {
  5435. $fields = array();
  5436. if (isset($params['order'])) {
  5437. foreach ($params['order'] as $order) {
  5438. $parts = explode(',', $order, 3);
  5439. $columnName = $parts[0];
  5440. if (!$table->hasColumn($columnName)) {
  5441. continue;
  5442. }
  5443. $ascending = 'ASC';
  5444. if (count($parts) > 1) {
  5445. if (substr(strtoupper($parts[1]), 0, 4) == "DESC") {
  5446. $ascending = 'DESC';
  5447. }
  5448. }
  5449. $fields[] = [$columnName, $ascending];
  5450. }
  5451. }
  5452. if (count($fields) == 0) {
  5453. return $this->getDefaultColumnOrdering($table);
  5454. }
  5455. return $fields;
  5456. }
  5457. public function getDefaultColumnOrdering(ReflectedTable $table): array
  5458. {
  5459. $fields = array();
  5460. $pk = $table->getPk();
  5461. if ($pk) {
  5462. $fields[] = [$pk->getName(), 'ASC'];
  5463. } else {
  5464. foreach ($table->getColumnNames() as $columnName) {
  5465. $fields[] = [$columnName, 'ASC'];
  5466. }
  5467. }
  5468. return $fields;
  5469. }
  5470. }
  5471. // file: src/Tqdev/PhpCrudApi/Record/PaginationInfo.php
  5472. class PaginationInfo
  5473. {
  5474. public $DEFAULT_PAGE_SIZE = 20;
  5475. public function hasPage(array $params): bool
  5476. {
  5477. return isset($params['page']);
  5478. }
  5479. public function getPageOffset(array $params): int
  5480. {
  5481. $offset = 0;
  5482. $pageSize = $this->getPageSize($params);
  5483. if (isset($params['page'])) {
  5484. foreach ($params['page'] as $page) {
  5485. $parts = explode(',', $page, 2);
  5486. $page = intval($parts[0]) - 1;
  5487. $offset = $page * $pageSize;
  5488. }
  5489. }
  5490. return $offset;
  5491. }
  5492. private function getPageSize(array $params): int
  5493. {
  5494. $pageSize = $this->DEFAULT_PAGE_SIZE;
  5495. if (isset($params['page'])) {
  5496. foreach ($params['page'] as $page) {
  5497. $parts = explode(',', $page, 2);
  5498. if (count($parts) > 1) {
  5499. $pageSize = intval($parts[1]);
  5500. }
  5501. }
  5502. }
  5503. return $pageSize;
  5504. }
  5505. public function getResultSize(array $params): int
  5506. {
  5507. $numberOfRows = -1;
  5508. if (isset($params['size'])) {
  5509. foreach ($params['size'] as $size) {
  5510. $numberOfRows = intval($size);
  5511. }
  5512. }
  5513. return $numberOfRows;
  5514. }
  5515. public function getPageLimit(array $params): int
  5516. {
  5517. $pageLimit = -1;
  5518. if ($this->hasPage($params)) {
  5519. $pageLimit = $this->getPageSize($params);
  5520. }
  5521. $resultSize = $this->getResultSize($params);
  5522. if ($resultSize >= 0) {
  5523. if ($pageLimit >= 0) {
  5524. $pageLimit = min($pageLimit, $resultSize);
  5525. } else {
  5526. $pageLimit = $resultSize;
  5527. }
  5528. }
  5529. return $pageLimit;
  5530. }
  5531. }
  5532. // file: src/Tqdev/PhpCrudApi/Record/PathTree.php
  5533. class PathTree implements \JsonSerializable
  5534. {
  5535. const WILDCARD = '*';
  5536. private $tree;
  5537. public function __construct( /* object */&$tree = null)
  5538. {
  5539. if (!$tree) {
  5540. $tree = $this->newTree();
  5541. }
  5542. $this->tree = &$tree;
  5543. }
  5544. public function newTree()
  5545. {
  5546. return (object) ['values' => [], 'branches' => (object) []];
  5547. }
  5548. public function getKeys(): array
  5549. {
  5550. $branches = (array) $this->tree->branches;
  5551. return array_keys($branches);
  5552. }
  5553. public function getValues(): array
  5554. {
  5555. return $this->tree->values;
  5556. }
  5557. public function get(string $key): PathTree
  5558. {
  5559. if (!isset($this->tree->branches->$key)) {
  5560. return null;
  5561. }
  5562. return new PathTree($this->tree->branches->$key);
  5563. }
  5564. public function put(array $path, $value)
  5565. {
  5566. $tree = &$this->tree;
  5567. foreach ($path as $key) {
  5568. if (!isset($tree->branches->$key)) {
  5569. $tree->branches->$key = $this->newTree();
  5570. }
  5571. $tree = &$tree->branches->$key;
  5572. }
  5573. $tree->values[] = $value;
  5574. }
  5575. public function match(array $path): array
  5576. {
  5577. $star = self::WILDCARD;
  5578. $tree = &$this->tree;
  5579. foreach ($path as $key) {
  5580. if (isset($tree->branches->$key)) {
  5581. $tree = &$tree->branches->$key;
  5582. } else if (isset($tree->branches->$star)) {
  5583. $tree = &$tree->branches->$star;
  5584. } else {
  5585. return [];
  5586. }
  5587. }
  5588. return $tree->values;
  5589. }
  5590. public static function fromJson( /* object */$tree): PathTree
  5591. {
  5592. return new PathTree($tree);
  5593. }
  5594. public function jsonSerialize()
  5595. {
  5596. return $this->tree;
  5597. }
  5598. }
  5599. // file: src/Tqdev/PhpCrudApi/Record/RecordService.php
  5600. class RecordService
  5601. {
  5602. private $db;
  5603. private $reflection;
  5604. private $columns;
  5605. private $joiner;
  5606. private $filters;
  5607. private $ordering;
  5608. private $pagination;
  5609. public function __construct(GenericDB $db, ReflectionService $reflection)
  5610. {
  5611. $this->db = $db;
  5612. $this->reflection = $reflection;
  5613. $this->columns = new ColumnIncluder();
  5614. $this->joiner = new RelationJoiner($reflection, $this->columns);
  5615. $this->filters = new FilterInfo();
  5616. $this->ordering = new OrderingInfo();
  5617. $this->pagination = new PaginationInfo();
  5618. }
  5619. private function sanitizeRecord(string $tableName, /* object */ $record, string $id)
  5620. {
  5621. $keyset = array_keys((array) $record);
  5622. foreach ($keyset as $key) {
  5623. if (!$this->reflection->getTable($tableName)->hasColumn($key)) {
  5624. unset($record->$key);
  5625. }
  5626. }
  5627. if ($id != '') {
  5628. $pk = $this->reflection->getTable($tableName)->getPk();
  5629. foreach ($this->reflection->getTable($tableName)->getColumnNames() as $key) {
  5630. $field = $this->reflection->getTable($tableName)->getColumn($key);
  5631. if ($field->getName() == $pk->getName()) {
  5632. unset($record->$key);
  5633. }
  5634. }
  5635. }
  5636. }
  5637. public function hasTable(string $table): bool
  5638. {
  5639. return $this->reflection->hasTable($table);
  5640. }
  5641. public function getType(string $table): string
  5642. {
  5643. return $this->reflection->getType($table);
  5644. }
  5645. public function create(string $tableName, /* object */ $record, array $params)
  5646. {
  5647. $this->sanitizeRecord($tableName, $record, '');
  5648. $table = $this->reflection->getTable($tableName);
  5649. $columnValues = $this->columns->getValues($table, true, $record, $params);
  5650. return $this->db->createSingle($table, $columnValues);
  5651. }
  5652. public function read(string $tableName, string $id, array $params) /*: ?object*/
  5653. {
  5654. $table = $this->reflection->getTable($tableName);
  5655. $this->joiner->addMandatoryColumns($table, $params);
  5656. $columnNames = $this->columns->getNames($table, true, $params);
  5657. $record = $this->db->selectSingle($table, $columnNames, $id);
  5658. if ($record == null) {
  5659. return null;
  5660. }
  5661. $records = array($record);
  5662. $this->joiner->addJoins($table, $records, $params, $this->db);
  5663. return $records[0];
  5664. }
  5665. public function update(string $tableName, string $id, /* object */ $record, array $params)
  5666. {
  5667. $this->sanitizeRecord($tableName, $record, $id);
  5668. $table = $this->reflection->getTable($tableName);
  5669. $columnValues = $this->columns->getValues($table, true, $record, $params);
  5670. return $this->db->updateSingle($table, $columnValues, $id);
  5671. }
  5672. public function delete(string $tableName, string $id, array $params)
  5673. {
  5674. $table = $this->reflection->getTable($tableName);
  5675. return $this->db->deleteSingle($table, $id);
  5676. }
  5677. public function increment(string $tableName, string $id, /* object */ $record, array $params)
  5678. {
  5679. $this->sanitizeRecord($tableName, $record, $id);
  5680. $table = $this->reflection->getTable($tableName);
  5681. $columnValues = $this->columns->getValues($table, true, $record, $params);
  5682. return $this->db->incrementSingle($table, $columnValues, $id);
  5683. }
  5684. public function _list(string $tableName, array $params): ListDocument
  5685. {
  5686. $table = $this->reflection->getTable($tableName);
  5687. $this->joiner->addMandatoryColumns($table, $params);
  5688. $columnNames = $this->columns->getNames($table, true, $params);
  5689. $condition = $this->filters->getCombinedConditions($table, $params);
  5690. $columnOrdering = $this->ordering->getColumnOrdering($table, $params);
  5691. if (!$this->pagination->hasPage($params)) {
  5692. $offset = 0;
  5693. $limit = $this->pagination->getPageLimit($params);
  5694. $count = 0;
  5695. } else {
  5696. $offset = $this->pagination->getPageOffset($params);
  5697. $limit = $this->pagination->getPageLimit($params);
  5698. $count = $this->db->selectCount($table, $condition);
  5699. }
  5700. $records = $this->db->selectAll($table, $columnNames, $condition, $columnOrdering, $offset, $limit);
  5701. $this->joiner->addJoins($table, $records, $params, $this->db);
  5702. return new ListDocument($records, $count);
  5703. }
  5704. }
  5705. // file: src/Tqdev/PhpCrudApi/Record/RelationJoiner.php
  5706. class RelationJoiner
  5707. {
  5708. private $reflection;
  5709. private $columns;
  5710. public function __construct(ReflectionService $reflection, ColumnIncluder $columns)
  5711. {
  5712. $this->reflection = $reflection;
  5713. $this->ordering = new OrderingInfo();
  5714. $this->columns = $columns;
  5715. }
  5716. public function addMandatoryColumns(ReflectedTable $table, array &$params) /*: void*/
  5717. {
  5718. if (!isset($params['join']) || !isset($params['include'])) {
  5719. return;
  5720. }
  5721. $params['mandatory'] = array();
  5722. foreach ($params['join'] as $tableNames) {
  5723. $t1 = $table;
  5724. foreach (explode(',', $tableNames) as $tableName) {
  5725. if (!$this->reflection->hasTable($tableName)) {
  5726. continue;
  5727. }
  5728. $t2 = $this->reflection->getTable($tableName);
  5729. $fks1 = $t1->getFksTo($t2->getName());
  5730. $t3 = $this->hasAndBelongsToMany($t1, $t2);
  5731. if ($t3 != null || count($fks1) > 0) {
  5732. $params['mandatory'][] = $t2->getName() . '.' . $t2->getPk()->getName();
  5733. }
  5734. foreach ($fks1 as $fk) {
  5735. $params['mandatory'][] = $t1->getName() . '.' . $fk->getName();
  5736. }
  5737. $fks2 = $t2->getFksTo($t1->getName());
  5738. if ($t3 != null || count($fks2) > 0) {
  5739. $params['mandatory'][] = $t1->getName() . '.' . $t1->getPk()->getName();
  5740. }
  5741. foreach ($fks2 as $fk) {
  5742. $params['mandatory'][] = $t2->getName() . '.' . $fk->getName();
  5743. }
  5744. $t1 = $t2;
  5745. }
  5746. }
  5747. }
  5748. private function getJoinsAsPathTree(array $params): PathTree
  5749. {
  5750. $joins = new PathTree();
  5751. if (isset($params['join'])) {
  5752. foreach ($params['join'] as $tableNames) {
  5753. $path = array();
  5754. foreach (explode(',', $tableNames) as $tableName) {
  5755. $t = $this->reflection->getTable($tableName);
  5756. if ($t != null) {
  5757. $path[] = $t->getName();
  5758. }
  5759. }
  5760. $joins->put($path, true);
  5761. }
  5762. }
  5763. return $joins;
  5764. }
  5765. public function addJoins(ReflectedTable $table, array &$records, array $params, GenericDB $db) /*: void*/
  5766. {
  5767. $joins = $this->getJoinsAsPathTree($params);
  5768. $this->addJoinsForTables($table, $joins, $records, $params, $db);
  5769. }
  5770. private function hasAndBelongsToMany(ReflectedTable $t1, ReflectedTable $t2) /*: ?ReflectedTable*/
  5771. {
  5772. foreach ($this->reflection->getTableNames() as $tableName) {
  5773. $t3 = $this->reflection->getTable($tableName);
  5774. if (count($t3->getFksTo($t1->getName())) > 0 && count($t3->getFksTo($t2->getName())) > 0) {
  5775. return $t3;
  5776. }
  5777. }
  5778. return null;
  5779. }
  5780. private function addJoinsForTables(ReflectedTable $t1, PathTree $joins, array &$records, array $params, GenericDB $db)
  5781. {
  5782. foreach ($joins->getKeys() as $t2Name) {
  5783. $t2 = $this->reflection->getTable($t2Name);
  5784. $belongsTo = count($t1->getFksTo($t2->getName())) > 0;
  5785. $hasMany = count($t2->getFksTo($t1->getName())) > 0;
  5786. if (!$belongsTo && !$hasMany) {
  5787. $t3 = $this->hasAndBelongsToMany($t1, $t2);
  5788. } else {
  5789. $t3 = null;
  5790. }
  5791. $hasAndBelongsToMany = ($t3 != null);
  5792. $newRecords = array();
  5793. $fkValues = null;
  5794. $pkValues = null;
  5795. $habtmValues = null;
  5796. if ($belongsTo) {
  5797. $fkValues = $this->getFkEmptyValues($t1, $t2, $records);
  5798. $this->addFkRecords($t2, $fkValues, $params, $db, $newRecords);
  5799. }
  5800. if ($hasMany) {
  5801. $pkValues = $this->getPkEmptyValues($t1, $records);
  5802. $this->addPkRecords($t1, $t2, $pkValues, $params, $db, $newRecords);
  5803. }
  5804. if ($hasAndBelongsToMany) {
  5805. $habtmValues = $this->getHabtmEmptyValues($t1, $t2, $t3, $db, $records);
  5806. $this->addFkRecords($t2, $habtmValues->fkValues, $params, $db, $newRecords);
  5807. }
  5808. $this->addJoinsForTables($t2, $joins->get($t2Name), $newRecords, $params, $db);
  5809. if ($fkValues != null) {
  5810. $this->fillFkValues($t2, $newRecords, $fkValues);
  5811. $this->setFkValues($t1, $t2, $records, $fkValues);
  5812. }
  5813. if ($pkValues != null) {
  5814. $this->fillPkValues($t1, $t2, $newRecords, $pkValues);
  5815. $this->setPkValues($t1, $t2, $records, $pkValues);
  5816. }
  5817. if ($habtmValues != null) {
  5818. $this->fillFkValues($t2, $newRecords, $habtmValues->fkValues);
  5819. $this->setHabtmValues($t1, $t2, $records, $habtmValues);
  5820. }
  5821. }
  5822. }
  5823. private function getFkEmptyValues(ReflectedTable $t1, ReflectedTable $t2, array $records): array
  5824. {
  5825. $fkValues = array();
  5826. $fks = $t1->getFksTo($t2->getName());
  5827. foreach ($fks as $fk) {
  5828. $fkName = $fk->getName();
  5829. foreach ($records as $record) {
  5830. if (isset($record[$fkName])) {
  5831. $fkValue = $record[$fkName];
  5832. $fkValues[$fkValue] = null;
  5833. }
  5834. }
  5835. }
  5836. return $fkValues;
  5837. }
  5838. private function addFkRecords(ReflectedTable $t2, array $fkValues, array $params, GenericDB $db, array &$records) /*: void*/
  5839. {
  5840. $columnNames = $this->columns->getNames($t2, false, $params);
  5841. $fkIds = array_keys($fkValues);
  5842. foreach ($db->selectMultiple($t2, $columnNames, $fkIds) as $record) {
  5843. $records[] = $record;
  5844. }
  5845. }
  5846. private function fillFkValues(ReflectedTable $t2, array $fkRecords, array &$fkValues) /*: void*/
  5847. {
  5848. $pkName = $t2->getPk()->getName();
  5849. foreach ($fkRecords as $fkRecord) {
  5850. $pkValue = $fkRecord[$pkName];
  5851. $fkValues[$pkValue] = $fkRecord;
  5852. }
  5853. }
  5854. private function setFkValues(ReflectedTable $t1, ReflectedTable $t2, array &$records, array $fkValues) /*: void*/
  5855. {
  5856. $fks = $t1->getFksTo($t2->getName());
  5857. foreach ($fks as $fk) {
  5858. $fkName = $fk->getName();
  5859. foreach ($records as $i => $record) {
  5860. if (isset($record[$fkName])) {
  5861. $key = $record[$fkName];
  5862. $records[$i][$fkName] = $fkValues[$key];
  5863. }
  5864. }
  5865. }
  5866. }
  5867. private function getPkEmptyValues(ReflectedTable $t1, array $records): array
  5868. {
  5869. $pkValues = array();
  5870. $pkName = $t1->getPk()->getName();
  5871. foreach ($records as $record) {
  5872. $key = $record[$pkName];
  5873. $pkValues[$key] = array();
  5874. }
  5875. return $pkValues;
  5876. }
  5877. private function addPkRecords(ReflectedTable $t1, ReflectedTable $t2, array $pkValues, array $params, GenericDB $db, array &$records) /*: void*/
  5878. {
  5879. $fks = $t2->getFksTo($t1->getName());
  5880. $columnNames = $this->columns->getNames($t2, false, $params);
  5881. $pkValueKeys = implode(',', array_keys($pkValues));
  5882. $conditions = array();
  5883. foreach ($fks as $fk) {
  5884. $conditions[] = new ColumnCondition($fk, 'in', $pkValueKeys);
  5885. }
  5886. $condition = OrCondition::fromArray($conditions);
  5887. $columnOrdering = array();
  5888. $limit = VariableStore::get("joinLimits.maxRecords") ?: -1;
  5889. if ($limit != -1) {
  5890. $columnOrdering = $this->ordering->getDefaultColumnOrdering($t2);
  5891. }
  5892. foreach ($db->selectAll($t2, $columnNames, $condition, $columnOrdering, 0, $limit) as $record) {
  5893. $records[] = $record;
  5894. }
  5895. }
  5896. private function fillPkValues(ReflectedTable $t1, ReflectedTable $t2, array $pkRecords, array &$pkValues) /*: void*/
  5897. {
  5898. $fks = $t2->getFksTo($t1->getName());
  5899. foreach ($fks as $fk) {
  5900. $fkName = $fk->getName();
  5901. foreach ($pkRecords as $pkRecord) {
  5902. $key = $pkRecord[$fkName];
  5903. if (isset($pkValues[$key])) {
  5904. $pkValues[$key][] = $pkRecord;
  5905. }
  5906. }
  5907. }
  5908. }
  5909. private function setPkValues(ReflectedTable $t1, ReflectedTable $t2, array &$records, array $pkValues) /*: void*/
  5910. {
  5911. $pkName = $t1->getPk()->getName();
  5912. $t2Name = $t2->getName();
  5913. foreach ($records as $i => $record) {
  5914. $key = $record[$pkName];
  5915. $records[$i][$t2Name] = $pkValues[$key];
  5916. }
  5917. }
  5918. private function getHabtmEmptyValues(ReflectedTable $t1, ReflectedTable $t2, ReflectedTable $t3, GenericDB $db, array $records): HabtmValues
  5919. {
  5920. $pkValues = $this->getPkEmptyValues($t1, $records);
  5921. $fkValues = array();
  5922. $fk1 = $t3->getFksTo($t1->getName())[0];
  5923. $fk2 = $t3->getFksTo($t2->getName())[0];
  5924. $fk1Name = $fk1->getName();
  5925. $fk2Name = $fk2->getName();
  5926. $columnNames = array($fk1Name, $fk2Name);
  5927. $pkIds = implode(',', array_keys($pkValues));
  5928. $condition = new ColumnCondition($t3->getColumn($fk1Name), 'in', $pkIds);
  5929. $columnOrdering = array();
  5930. $limit = VariableStore::get("joinLimits.maxRecords") ?: -1;
  5931. if ($limit != -1) {
  5932. $columnOrdering = $this->ordering->getDefaultColumnOrdering($t3);
  5933. }
  5934. $records = $db->selectAll($t3, $columnNames, $condition, $columnOrdering, 0, $limit);
  5935. foreach ($records as $record) {
  5936. $val1 = $record[$fk1Name];
  5937. $val2 = $record[$fk2Name];
  5938. $pkValues[$val1][] = $val2;
  5939. $fkValues[$val2] = null;
  5940. }
  5941. return new HabtmValues($pkValues, $fkValues);
  5942. }
  5943. private function setHabtmValues(ReflectedTable $t1, ReflectedTable $t2, array &$records, HabtmValues $habtmValues) /*: void*/
  5944. {
  5945. $pkName = $t1->getPk()->getName();
  5946. $t2Name = $t2->getName();
  5947. foreach ($records as $i => $record) {
  5948. $key = $record[$pkName];
  5949. $val = array();
  5950. $fks = $habtmValues->pkValues[$key];
  5951. foreach ($fks as $fk) {
  5952. $val[] = $habtmValues->fkValues[$fk];
  5953. }
  5954. $records[$i][$t2Name] = $val;
  5955. }
  5956. }
  5957. }
  5958. // file: src/Tqdev/PhpCrudApi/Api.php
  5959. class Api
  5960. {
  5961. private $router;
  5962. private $responder;
  5963. private $debug;
  5964. public function __construct(Config $config)
  5965. {
  5966. $db = new GenericDB(
  5967. $config->getDriver(),
  5968. $config->getAddress(),
  5969. $config->getPort(),
  5970. $config->getDatabase(),
  5971. $config->getUsername(),
  5972. $config->getPassword()
  5973. );
  5974. $cache = CacheFactory::create($config);
  5975. $reflection = new ReflectionService($db, $cache, $config->getCacheTime());
  5976. $responder = new Responder();
  5977. $router = new SimpleRouter($responder, $cache, $config->getCacheTime(), $config->getDebug());
  5978. foreach ($config->getMiddlewares() as $middleware => $properties) {
  5979. switch ($middleware) {
  5980. case 'cors':
  5981. new CorsMiddleware($router, $responder, $properties);
  5982. break;
  5983. case 'firewall':
  5984. new FirewallMiddleware($router, $responder, $properties);
  5985. break;
  5986. case 'basicAuth':
  5987. new BasicAuthMiddleware($router, $responder, $properties);
  5988. break;
  5989. case 'jwtAuth':
  5990. new JwtAuthMiddleware($router, $responder, $properties);
  5991. break;
  5992. case 'validation':
  5993. new ValidationMiddleware($router, $responder, $properties, $reflection);
  5994. break;
  5995. case 'ipAddress':
  5996. new IpAddressMiddleware($router, $responder, $properties, $reflection);
  5997. break;
  5998. case 'sanitation':
  5999. new SanitationMiddleware($router, $responder, $properties, $reflection);
  6000. break;
  6001. case 'multiTenancy':
  6002. new MultiTenancyMiddleware($router, $responder, $properties, $reflection);
  6003. break;
  6004. case 'authorization':
  6005. new AuthorizationMiddleware($router, $responder, $properties, $reflection);
  6006. break;
  6007. case 'xsrf':
  6008. new XsrfMiddleware($router, $responder, $properties);
  6009. break;
  6010. case 'pageLimits':
  6011. new PageLimitsMiddleware($router, $responder, $properties, $reflection);
  6012. break;
  6013. case 'joinLimits':
  6014. new JoinLimitsMiddleware($router, $responder, $properties, $reflection);
  6015. break;
  6016. case 'customization':
  6017. new CustomizationMiddleware($router, $responder, $properties, $reflection);
  6018. break;
  6019. }
  6020. }
  6021. foreach ($config->getControllers() as $controller) {
  6022. switch ($controller) {
  6023. case 'records':
  6024. $records = new RecordService($db, $reflection);
  6025. new RecordController($router, $responder, $records);
  6026. break;
  6027. case 'columns':
  6028. $definition = new DefinitionService($db, $reflection);
  6029. new ColumnController($router, $responder, $reflection, $definition);
  6030. break;
  6031. case 'cache':
  6032. new CacheController($router, $responder, $cache);
  6033. break;
  6034. case 'openapi':
  6035. $openApi = new OpenApiService($reflection, $config->getOpenApiBase());
  6036. new OpenApiController($router, $responder, $openApi);
  6037. break;
  6038. }
  6039. }
  6040. $this->router = $router;
  6041. $this->responder = $responder;
  6042. $this->debug = $config->getDebug();
  6043. }
  6044. public function handle(ServerRequestInterface $request): ResponseInterface
  6045. {
  6046. $response = null;
  6047. try {
  6048. $response = $this->router->route($request);
  6049. } catch (\Throwable $e) {
  6050. $response = $this->responder->error(ErrorCode::ERROR_NOT_FOUND, $e->getMessage());
  6051. if ($this->debug) {
  6052. $response = ResponseUtils::addExceptionHeaders($response, $e);
  6053. }
  6054. }
  6055. return $response;
  6056. }
  6057. }
  6058. // file: src/Tqdev/PhpCrudApi/Config.php
  6059. class Config
  6060. {
  6061. private $values = [
  6062. 'driver' => null,
  6063. 'address' => 'localhost',
  6064. 'port' => null,
  6065. 'username' => null,
  6066. 'password' => null,
  6067. 'database' => null,
  6068. 'middlewares' => 'cors',
  6069. 'controllers' => 'records,openapi',
  6070. 'cacheType' => 'TempFile',
  6071. 'cachePath' => '',
  6072. 'cacheTime' => 10,
  6073. 'debug' => false,
  6074. 'openApiBase' => '{"info":{"title":"PHP-CRUD-API","version":"1.0.0"}}',
  6075. ];
  6076. private function getDefaultDriver(array $values): string
  6077. {
  6078. if (isset($values['driver'])) {
  6079. return $values['driver'];
  6080. }
  6081. return 'mysql';
  6082. }
  6083. private function getDefaultPort(string $driver): int
  6084. {
  6085. switch ($driver) {
  6086. case 'mysql':return 3306;
  6087. case 'pgsql':return 5432;
  6088. case 'sqlsrv':return 1433;
  6089. }
  6090. }
  6091. private function getDefaultAddress(string $driver): string
  6092. {
  6093. switch ($driver) {
  6094. case 'mysql':return 'localhost';
  6095. case 'pgsql':return 'localhost';
  6096. case 'sqlsrv':return 'localhost';
  6097. }
  6098. }
  6099. private function getDriverDefaults(string $driver): array
  6100. {
  6101. return [
  6102. 'driver' => $driver,
  6103. 'address' => $this->getDefaultAddress($driver),
  6104. 'port' => $this->getDefaultPort($driver),
  6105. ];
  6106. }
  6107. public function __construct(array $values)
  6108. {
  6109. $driver = $this->getDefaultDriver($values);
  6110. $defaults = $this->getDriverDefaults($driver);
  6111. $newValues = array_merge($this->values, $defaults, $values);
  6112. $newValues = $this->parseMiddlewares($newValues);
  6113. $diff = array_diff_key($newValues, $this->values);
  6114. if (!empty($diff)) {
  6115. $key = array_keys($diff)[0];
  6116. throw new \Exception("Config has invalid value '$key'");
  6117. }
  6118. $this->values = $newValues;
  6119. }
  6120. private function parseMiddlewares(array $values): array
  6121. {
  6122. $newValues = array();
  6123. $properties = array();
  6124. $middlewares = array_map('trim', explode(',', $values['middlewares']));
  6125. foreach ($middlewares as $middleware) {
  6126. $properties[$middleware] = [];
  6127. }
  6128. foreach ($values as $key => $value) {
  6129. if (strpos($key, '.') === false) {
  6130. $newValues[$key] = $value;
  6131. } else {
  6132. list($middleware, $key2) = explode('.', $key, 2);
  6133. if (isset($properties[$middleware])) {
  6134. $properties[$middleware][$key2] = $value;
  6135. } else {
  6136. throw new \Exception("Config has invalid value '$key'");
  6137. }
  6138. }
  6139. }
  6140. $newValues['middlewares'] = array_reverse($properties, true);
  6141. return $newValues;
  6142. }
  6143. public function getDriver(): string
  6144. {
  6145. return $this->values['driver'];
  6146. }
  6147. public function getAddress(): string
  6148. {
  6149. return $this->values['address'];
  6150. }
  6151. public function getPort(): int
  6152. {
  6153. return $this->values['port'];
  6154. }
  6155. public function getUsername(): string
  6156. {
  6157. return $this->values['username'];
  6158. }
  6159. public function getPassword(): string
  6160. {
  6161. return $this->values['password'];
  6162. }
  6163. public function getDatabase(): string
  6164. {
  6165. return $this->values['database'];
  6166. }
  6167. public function getMiddlewares(): array
  6168. {
  6169. return $this->values['middlewares'];
  6170. }
  6171. public function getControllers(): array
  6172. {
  6173. return array_map('trim', explode(',', $this->values['controllers']));
  6174. }
  6175. public function getCacheType(): string
  6176. {
  6177. return $this->values['cacheType'];
  6178. }
  6179. public function getCachePath(): string
  6180. {
  6181. return $this->values['cachePath'];
  6182. }
  6183. public function getCacheTime(): int
  6184. {
  6185. return $this->values['cacheTime'];
  6186. }
  6187. public function getDebug(): bool
  6188. {
  6189. return $this->values['debug'];
  6190. }
  6191. public function getOpenApiBase(): array
  6192. {
  6193. return json_decode($this->values['openApiBase'], true);
  6194. }
  6195. }
  6196. // file: src/Tqdev/PhpCrudApi/RequestFactory.php
  6197. class RequestFactory
  6198. {
  6199. private static function parseBody(string $body) /*: ?object*/
  6200. {
  6201. $first = substr($body, 0, 1);
  6202. if ($first == '[' || $first == '{') {
  6203. $object = json_decode($body);
  6204. $causeCode = json_last_error();
  6205. if ($causeCode !== JSON_ERROR_NONE) {
  6206. $object = null;
  6207. }
  6208. } else {
  6209. parse_str($body, $input);
  6210. foreach ($input as $key => $value) {
  6211. if (substr($key, -9) == '__is_null') {
  6212. $input[substr($key, 0, -9)] = null;
  6213. unset($input[$key]);
  6214. }
  6215. }
  6216. $object = (object) $input;
  6217. }
  6218. return $object;
  6219. }
  6220. public static function fromGlobals(): ServerRequestInterface
  6221. {
  6222. $psr17Factory = new Psr17Factory();
  6223. $creator = new ServerRequestCreator($psr17Factory, $psr17Factory, $psr17Factory, $psr17Factory);
  6224. $serverRequest = $creator->fromGlobals();
  6225. $uri = '';
  6226. if (isset($_SERVER['PATH_INFO'])) {
  6227. $uri .= $_SERVER['PATH_INFO'];
  6228. }
  6229. if (isset($_SERVER['QUERY_STRING'])) {
  6230. $uri .= '?' . $_SERVER['QUERY_STRING'];
  6231. }
  6232. if ($uri) {
  6233. $serverRequest = $serverRequest->withUri($psr17Factory->createUri($uri));
  6234. }
  6235. $body = file_get_contents('php://input');
  6236. if ($body) {
  6237. $serverRequest = $serverRequest->withParsedBody(self::parseBody($body));
  6238. }
  6239. return $serverRequest;
  6240. }
  6241. public static function fromString(string $request): ServerRequestInterface
  6242. {
  6243. $parts = explode("\n\n", trim($request), 2);
  6244. $lines = explode("\n", $parts[0]);
  6245. $first = explode(' ', trim(array_shift($lines)), 2);
  6246. $method = $first[0];
  6247. $body = isset($parts[1]) ? $parts[1] : '';
  6248. $url = isset($first[1]) ? $first[1] : '';
  6249. $psr17Factory = new Psr17Factory();
  6250. $serverRequest = $psr17Factory->createServerRequest($method, $url);
  6251. foreach ($lines as $line) {
  6252. list($key, $value) = explode(':', $line, 2);
  6253. $serverRequest = $serverRequest->withAddedHeader($key, $value);
  6254. }
  6255. if ($body) {
  6256. $stream = $psr17Factory->createStream($body);
  6257. $stream->rewind();
  6258. $serverRequest = $serverRequest->withBody($stream);
  6259. $serverRequest = $serverRequest->withParsedBody(self::parseBody($body));
  6260. }
  6261. return $serverRequest;
  6262. }
  6263. }
  6264. // file: src/Tqdev/PhpCrudApi/RequestUtils.php
  6265. class RequestUtils
  6266. {
  6267. public static function setParams(ServerRequestInterface $request, array $params): ServerRequestInterface
  6268. {
  6269. $query = preg_replace('|%5B[0-9]+%5D=|', '=', http_build_query($params));
  6270. return $request->withUri($request->getUri()->withQuery($query));
  6271. }
  6272. public static function getHeader(ServerRequestInterface $request, string $header): string
  6273. {
  6274. $headers = $request->getHeader($header);
  6275. return isset($headers[0]) ? $headers[0] : '';
  6276. }
  6277. public static function getParams(ServerRequestInterface $request): array
  6278. {
  6279. $params = array();
  6280. $query = $request->getUri()->getQuery();
  6281. $query = str_replace('][]=', ']=', str_replace('=', '[]=', $query));
  6282. parse_str($query, $params);
  6283. return $params;
  6284. }
  6285. public static function getPathSegment(ServerRequestInterface $request, int $part): string
  6286. {
  6287. $pathSegments = explode('/', rtrim($request->getUri()->getPath(), '/'));
  6288. if ($part < 0 || $part >= count($pathSegments)) {
  6289. return '';
  6290. }
  6291. return urldecode($pathSegments[$part]);
  6292. }
  6293. public static function getOperation(ServerRequestInterface $request): string
  6294. {
  6295. $method = $request->getMethod();
  6296. $path = RequestUtils::getPathSegment($request, 1);
  6297. $hasPk = RequestUtils::getPathSegment($request, 3) != '';
  6298. switch ($path) {
  6299. case 'openapi':
  6300. return 'document';
  6301. case 'columns':
  6302. return $method == 'get' ? 'reflect' : 'remodel';
  6303. case 'records':
  6304. switch ($method) {
  6305. case 'POST':
  6306. return 'create';
  6307. case 'GET':
  6308. return $hasPk ? 'read' : 'list';
  6309. case 'PUT':
  6310. return 'update';
  6311. case 'DELETE':
  6312. return 'delete';
  6313. case 'PATCH':
  6314. return 'increment';
  6315. }
  6316. }
  6317. return 'unknown';
  6318. }
  6319. private static function getJoinTables(string $tableName, array $parameters): array
  6320. {
  6321. $uniqueTableNames = array();
  6322. $uniqueTableNames[$tableName] = true;
  6323. if (isset($parameters['join'])) {
  6324. foreach ($parameters['join'] as $parameter) {
  6325. $tableNames = explode(',', trim($parameter));
  6326. foreach ($tableNames as $tableName) {
  6327. $uniqueTableNames[$tableName] = true;
  6328. }
  6329. }
  6330. }
  6331. return array_keys($uniqueTableNames);
  6332. }
  6333. public static function getTableNames(ServerRequestInterface $request, ReflectionService $reflection): array
  6334. {
  6335. $path = RequestUtils::getPathSegment($request, 1);
  6336. $tableName = RequestUtils::getPathSegment($request, 2);
  6337. $allTableNames = $reflection->getTableNames();
  6338. switch ($path) {
  6339. case 'openapi':
  6340. return $allTableNames;
  6341. case 'columns':
  6342. return $tableName ? [$tableName] : $allTableNames;
  6343. case 'records':
  6344. return self::getJoinTables($tableName, RequestUtils::getParams($request));
  6345. }
  6346. return $allTableNames;
  6347. }
  6348. }
  6349. // file: src/Tqdev/PhpCrudApi/ResponseFactory.php
  6350. class ResponseFactory
  6351. {
  6352. const OK = 200;
  6353. const UNAUTHORIZED = 401;
  6354. const FORBIDDEN = 403;
  6355. const NOT_FOUND = 404;
  6356. const METHOD_NOT_ALLOWED = 405;
  6357. const CONFLICT = 409;
  6358. const UNPROCESSABLE_ENTITY = 422;
  6359. const INTERNAL_SERVER_ERROR = 500;
  6360. public static function fromObject(int $status, $body): ResponseInterface
  6361. {
  6362. $psr17Factory = new Psr17Factory();
  6363. $response = $psr17Factory->createResponse($status);
  6364. $content = json_encode($body, JSON_UNESCAPED_UNICODE);
  6365. $stream = $psr17Factory->createStream($content);
  6366. $stream->rewind();
  6367. $response = $response->withBody($stream);
  6368. $response = $response->withHeader('Content-Type', 'application/json');
  6369. $response = $response->withHeader('Content-Length', strlen($content));
  6370. return $response;
  6371. }
  6372. public static function fromStatus(int $status): ResponseInterface
  6373. {
  6374. $psr17Factory = new Psr17Factory();
  6375. return $psr17Factory->createResponse($status);
  6376. }
  6377. }
  6378. // file: src/Tqdev/PhpCrudApi/ResponseUtils.php
  6379. class ResponseUtils
  6380. {
  6381. public static function output(ResponseInterface $response)
  6382. {
  6383. $status = $response->getStatusCode();
  6384. $headers = $response->getHeaders();
  6385. $body = $response->getBody()->getContents();
  6386. http_response_code($status);
  6387. foreach ($headers as $key => $values) {
  6388. foreach ($values as $value) {
  6389. header("$key: $value");
  6390. }
  6391. }
  6392. echo $body;
  6393. }
  6394. public static function addExceptionHeaders(ResponseInterface $response, \Throwable $e): ResponseInterface
  6395. {
  6396. $response = $response->withHeader('X-Exception-Name', get_class($e));
  6397. $response = $response->withHeader('X-Exception-Message', $e->getMessage());
  6398. $response = $response->withHeader('X-Exception-File', $e->getFile() . ':' . $e->getLine());
  6399. return $response;
  6400. }
  6401. public static function toString(ResponseInterface $response): string
  6402. {
  6403. $status = $response->getStatusCode();
  6404. $headers = $response->getHeaders();
  6405. $body = $response->getBody()->getContents();
  6406. $str = "$status\n";
  6407. foreach ($headers as $key => $values) {
  6408. foreach ($values as $value) {
  6409. $str .= "$key: $value\n";
  6410. }
  6411. }
  6412. if ($body !== '') {
  6413. $str .= "\n";
  6414. $str .= "$body\n";
  6415. }
  6416. return $str;
  6417. }
  6418. }
  6419. // file: src/index.php
  6420. $config = new Config([
  6421. 'username' => 'php-crud-api',
  6422. 'password' => 'php-crud-api',
  6423. 'database' => 'php-crud-api',
  6424. ]);
  6425. $request = RequestFactory::fromGlobals();
  6426. $api = new Api($config);
  6427. $response = $api->handle($request);
  6428. ResponseUtils::output($response);